# 1.4.3 (10.08.26)
- `nearest_ranges` takes `ties`, on both `PyRanges` and `RangeFrame`. The default, `"all"`, is unchanged: every interval of `other` at the winning distance is reported. `ties="first"` reports one of them, so each interval of self appears at most once. Which one is not specified — only that the same input gives the same answer every time; nothing is sorted to decide it, because the sort would cost more than the option saves
- this matters most where it is least visible. Overlapping intervals are all at distance 0, so with the default `exclude_overlaps=False` "every tied interval" means "every interval of other that self overlaps": one interval of self covered by 500 intervals of other produces 500 rows, all of them equally the nearest. On 100 million hg38-like intervals against themselves that is 1.1 billion output rows where one per query would be 7.7 million — 144x — which is the difference between fitting in memory and not
- the tied rows are never built, so `ties="first"` saves the memory and not only the time. Materialising them and keeping one per query afterwards would have cost the same as `"all"` and more time; instead the overlap sweep stops at the first hit per interval of self, each directional sweep emits one row per distinct distance, and the merge keeps one row per distance bucket. That work is in `ruranges_core::nearest`, so the floor on `ruranges` rises to 0.1.8
- `k` still counts distinct distances, not rows. `ties="first"` with `k=2` reports two rows, one per distance, rather than the combination being rejected
- `ties` names the same thing here as elsewhere in the field: bedtools spells it `closest -t first`, GenomicRanges `select="arbitrary"`, BEDOPS `--closest`. bioframe, polars-bio and pyranges 0.x report one interval per query and offer no way to ask for the other behaviour
- an unknown `ties` raises `ValueError` naming the valid options, as `direction` and `multiple` already do, rather than reaching the kernel: a Rust panic is not an `Exception`, so `except Exception` cannot catch it and the interpreter is aborted

# 1.4.2 (31.07.26)
- **fix, severe**: `PyRanges.nearest_ranges(direction="upstream"/"downstream")` ignored strand on minus-strand intervals, returning the neighbour on the wrong side and so inverting the biological meaning of every directional nearest query on the reverse strand — nearest promoter, nearest TSS. For a minus-strand feature, upstream is the *higher* coordinate, as `PyRanges.upstream()` has always had it. `self` was split into its forward and reverse halves and both halves were then queried with the same coordinate direction. The strand-to-coordinate-direction mapping is now a single four-entry table rather than four hand-picked direction strings at four call sites, which is what let the bug through. Reported and fixed by Mike German (@steps-re) in gh-162
- **fix**: `RangeFrame.nearest_ranges` did not validate `direction`. Any value the coordinate space does not know — including the strand-aware `"upstream"`/`"downstream"`, which belong to `PyRanges` — reached `ruranges` and raised a `PanicException` reading `Invalid direction string`. That is not an `Exception`, so `except Exception` could not catch it and the interpreter was aborted. It now raises `ValueError` naming the valid options, as `multiple` has since 1.4.0
- **fix**: `PyRanges.nearest_ranges` with a directional `direction` on a frame without valid strand failed with `UndefinedVariableError: name 'Strand' is not defined`, leaked from a pandas query. It now raises `ValueError` explaining that upstream and downstream are strand-aware. `direction="any"` is unaffected
- the two direction vocabularies are named for what they are: `VALID_NEAREST_TYPE` is now `VALID_GENOMIC_DIRECTION_TYPE` (`any`/`upstream`/`downstream`, strand-aware) and `VALID_DIRECTION_TYPE` is now `VALID_COORDINATE_DIRECTION_TYPE` (`any`/`forward`/`backward`, where forward is always the higher coordinate). Both are internal to `pyranges1.core.names`. `VALID_NEAREST_OPTIONS` listed `upstream` twice and never `downstream`; it was unused, and the corrected list now backs the validation above
- **fix, gh-166**: `strand_behavior="opposite"` dropped every non-location column of `other`. To flip the strand, `prepare_by_binary` took `other.loc[:, [*RANGE_COLS, *by]].copy()` — a copy that was also a projection — so `join_overlaps` and `nearest_ranges` lost the columns they exist to report. `left.join_overlaps(right, strand_behavior="opposite")` returned no `ID_b`/`Score_b` at all, while the same call with `"same"` returned both, making the loss depend on the strand option rather than on the data. Downstream code indexing those columns raised `KeyError` for one strand setting and worked for the others. The copy is now of the whole frame; the projection was never needed
- **fix**: `complement_ranges` emitted intervals of non-positive length at the externally bounded edges — an interval no validator in the library accepts. With `chromsizes` and a last interval ending exactly on the chromosome size it emitted a terminal gap of zero length (`Start == End == size`); that gap is now dropped, as is any other gap the kernel returns with `End <= Start`
- **breaking**: `complement_ranges(include_first_interval=True)` now raises `ValueError` when any interval starts below zero. The first complement interval runs from coordinate 0 to the first interval, so a negative start produced a gap of *negative* length: one row `chr1:[-1, 0)+` came back as `Start=0, End=-1`. Negative coordinates remain legal everywhere else — `extend_ranges`, `upstream` and `five_end` all produce them by design — this is checked only where it would produce an invalid result
- **fix**: `join_overlaps` with `report_overlap_column` reported an overlap for rows that matched nothing. `DataFrame.min`/`.max` skip nulls, so an unmatched row of a `left`, `right` or `outer` join got the length of the one interval present — `Overlap = 1` on a row whose every `_b` column is null. Unmatched rows now report null
- **breaking**: `split_overlaps` no longer drops `Strand` when `use_strand=False`. It dropped the column merely because it was not a grouping key, so `gr.split_overlaps(use_strand=False)` lost strand information the input carried and no other method discards this way. Every output interval descends from an input interval and now keeps its metadata. `between=True` is unchanged: gap intervals descend from no input row, so they keep only the location columns, and `Strand` among them only when it was a grouping key
- **breaking**: `group_cumsum` accumulated across chromosomes. With no `group_by` it ran a single cumulative sum over the whole frame, so two rows of length 10 on `chr1` and `chr2` came back as `[0,10)` and `[10,20)` — one coordinate space spanning two chromosomes. `Chromosome`, and `Strand` when strand-aware, now always partition the cumulative space. The documented behaviour ("when *None* all intervals on the same chromosome are cumulated together") was already this; only the code disagreed. Naming a column in `group_by` no longer drops the chromosome partition either. The caller's own keys are placed *before* the implicit ones, because the key order decides how groups are numbered and therefore the order rows come back in when `keep_order=False`; only the key set decides which intervals share a cumulative space. `map_to_local`, `map_to_global` and `tile_ranges(add_window_id=True)` all call `group_cumsum` internally with a transcript-level key, and their output row order is unchanged by this fix
- **breaking**: `to_bed`, `to_gtf` and `to_gff3` always infer compression from the file extension. The default was `None`, which pandas reads as *no compression even for a .gz path*, while all three docstrings said "infer" — `to_bed("x.bed.gz")` silently wrote plain text under a `.gz` name. `None` is now treated as `"infer"`, so there is no longer a way to write uncompressed output to a `.gz` path; pass a different extension

# 1.4.1 (31.07.26)
- **fix, severe**: `RangeFrame.sort_ranges` passed the kernel's arguments in the wrong order — group ids landed in `starts`, `Start` in `ends` and `End` in `groups` — so the primary sort key was `End`, not `Start`. On a 100,000-row frame with realistic coordinates, 97,964 rows came back in the wrong position. `PyRanges.sort_ranges` was unaffected. The `groups` argument of `ruranges.numpy.sort_intervals` is keyword-only from `ruranges>=0.1.7`, so the transposition can no longer happen silently
- **fix, severe**: `RangeFrame.sort_ranges()` with no `by` ran a full Python-level pass over every row to build an array that is provably all zeros — 97% of the total cost of an unkeyed sort (5.77 s of 5.95 s at 5,000,000 rows). The replacement ranker is never invoked for zero columns
- **breaking**: `RangeFrame.sort_ranges` no longer takes `natsort`, and orders string keys lexically. Natural ordering exists for chromosome names, and a `RangeFrame` has no `Chromosome` column; `PyRanges.sort_ranges` keeps `natsort` unchanged. `rf.sort_ranges(by="transcript_id")` therefore now puts `t10` before `t9`
- **breaking**: `RangeFrame.sort_ranges` no longer takes `sort_rows_reverse_order`. It was declared and never called, and it was the one parameter in the family requiring a per-row Python sequence. Callers wanting 5'→3' ordering use `PyRanges.sort_ranges`
- both `sort_ranges` methods take `by` keys that *relocate*: the full key list is `Chromosome, Strand, *by, Start, End`, and any column named in `by` is taken out of its implicit position and used where you put it. `by=["Strand", "Chromosome"]` sorts by strand first; `by=["Start", "End", "score"]` sorts by a key *after* the coordinates. Both halves of gh-94. The rule applies per column, so `by=["score", "Chromosome"]` gives `Strand, score, Chromosome, Start, End` — name every key you care about
- both `sort_ranges` methods take `sort_descending`, naming keys to reverse. Any key qualifies, the implicit `Chromosome`, `Strand`, `Start` and `End` included. A name that is not a sort key raises `ValueError` rather than being ignored. On the coordinate keys it composes with `use_strand` by XOR, so a reversed row and a reversed key cancel out
- **deprecated**: `RangeFrame.sort_by_position` now warns and will be removed next release. `sort_ranges()` with no arguments sorts by the same columns, and on `PyRanges` `sort_by_position` is a trap: it sorts globally by position and ignores `Chromosome`
- sorting is faster and the ordering rule is now shared with `polaranges`. Key columns are coded one at a time rather than as key tuples — the cost is the number of distinct values per column, not the number of distinct combinations, which approaches the row count — and the natural ordering of a column's distinct string values happens in Rust, in `ruranges_core::ranks`, which `polaranges` also calls. Doing it in Python cost 24.9 s for 10 million distinct values against 0.7 s in Rust and was the largest line item in every large sort. `natsort` is no longer a runtime dependency of the sort path
- document that `use_strand=False` does not remove `Strand` from the sort keys: it only stops negative-strand rows being ordered 3'→5'. To sort without grouping by strand, drop or rename the column

# 1.4.0 (30.07.26)
- **breaking**: `RangeFrame.overlap` now defaults to `multiple=False`, matching `PyRanges.overlap`. It previously defaulted to `"all"`, so `rf.overlap(other)` and `gr.overlap(other)` — the same bare call on a class and its subclass — meant different things: one reported every overlapping pair, the other filtered. A bare `overlap()` is now a filter everywhere. Pass `multiple=True` for the old generic behaviour
- `RangeFrame.overlap` also takes `multiple` as a bool now, like `PyRanges.overlap`. `overlap` returns rows of self and nothing from other, so the only thing this option can change is how many times a row appears: `"first"` and `"last"` would select the same rows and differ in output order alone. A string now raises `TypeError` instead of being silently truthy, and the option is keyword-only on both
- `PyRanges.overlap` used to derive the string the kernel wants with a bare truthiness test, so `multiple="first"` and `multiple="last"` silently meant `"all"` — the opposite of what was asked, with no error. On a 200k x 500k overlap that is a 47% row-count difference
- `intersect_overlaps`, `join_overlaps` and `set_intersect_overlaps` keep the `{"all", "first", "last"}` vocabulary: they return information taken from other, so `"first"` and `"last"` genuinely select different output there
- reject an unknown `multiple` with a `ValueError` naming the valid options; it used to reach `ruranges` and abort the interpreter with a Rust panic reading `invalid overlap_type string: "Invalid direction string"`, which `except Exception` cannot catch
- correct the `multiple` docstrings that described `overlap` and `join_overlaps` as reporting "subintervals". Only `intersect_overlaps`, `set_intersect_overlaps` and `subtract_overlaps` return subintervals; `overlap` returns whole intervals of self and `join_overlaps` returns them alongside other's columns
- remove `"contained"` from `VALID_OVERLAP_TYPE`. It was declared but `ruranges>=0.1.5` rejects the overlap type, so it only ever panicked, and it duplicated `contained_intervals_only=True`, which is now the single way to express containment. `multiple="contained"` raises a `ValueError` naming that argument
- annotate `tile_ranges`'s `use_strand` as `VALID_USE_STRAND_TYPE`, like every other method taking it. It was annotated `bool` while already accepting and handling `"auto"` at runtime, so type checkers rejected a valid call

# 1.3.11 (30.07.26)
- fix `to_bigwig(divide=True)` silently dropping any chromosome whose divided track reduces to a single run; `pyrle`'s `defragment` zeroes the value in that case and `to_ranges` then discards it as uncovered, so e.g. a one-interval frame with `Value=2` over coverage 1 returned no rows instead of log2(2) (gh-160)
- remove the dead `new_pyrles` accumulator in the same branch, which was built and discarded, and the redundant per-track `defragment()` call whose return value was thrown away
- removed the only doctest example showcasing a negative slack value, which isn't supported

# 1.3.10 (27.07.26)
- require `ruranges>=0.1.5`, which fixes `complement_ranges` attributing each gap to an arbitrary row's grouping columns (gh-157)
- `loci` now leaves an omitted slice bound genuinely open; the lower bound was clamped to a finite value, so `gr.loci[:n]` silently dropped intervals lying entirely below it
- remove `copy=False` from an `astype` call that failed pyright CI; add tox `[gh]` section

# 1.3.9 (30.05.26)
- fix `join_overlaps` duplicating rows in `left`/`outer` joins when the input index did not start at 0
- preserve the input index in `join_overlaps` output (self for inner/left/outer, other for right), consistent with `overlap`/`nearest_ranges`

# 1.3.8 (21.04.26)
- repo name changed from pyranges_1.x to pyranges1
- updated references to it
- fix issue 151 (Proper use of args and kwargs in concat)

# 1.3.7 (16.04.26)
- require `ruranges>=0.1.4`
- add `preserve_input_order` to Rust-backed overlap-style operations so large results can skip the extra output reordering step
- document the new output-order option with updated docstrings and doctest examples

# 1.3.6 (27.03.26)
- require `ruranges>=0.1.3`
- pick up the `ruranges` fix for `contained_intervals_only=True` overlaps when intervals share the same start coordinate
- fix `merge_overlaps` docs to refer to `count_col` instead of a nonexistent `count` parameter

# 1.3.5 (18.03.26)
- move GTF reading and attribute parsing onto the new `gtfreader>=0.1.1` dependency
- preserve semicolons inside quoted GTF attributes and keep duplicate quoted attributes in `to_rows_keep_duplicates`
- read GTF `Source` and `Frame` columns as categorical dtypes
- update GTF docs and doctests to reflect the new categorical display and supported duplicate-attribute format
- speed up GTF parsing

# 1.3.4 (14.03.26)
- accept ; in quotes in gtf attrs
- also speed up gtf parsing

# 1.3.3 (13.03.26)
- nearest_ranges: treat touching intervals as nearest matches with distance 1 instead of overlapping matches
- document touching-interval nearest behavior in doctests

# 1.3.2 (27.02.26)
- pandas 3 compatibility: removed pandas<3 constraint and aligned test matrix/dependencies
- update doctests/unit test expectations to pandas 3 native formatting (including `str` dtype display)
- remove dtype display normalization workaround in table rendering
- fix pandas 3 copy-on-write/read-only array issue in coverage path used by bigwig/rle conversion
- improve groupby `prod` compatibility across pandas 2/3 edge cases

# 1.3.1 (15.02.26)
- require `ruranges>=0.1.1`
- remove fallback compatibility hack for old `ruranges` installs that lacked `ruranges.numpy`
- add runtime guard with clear error for incompatible/too-old `ruranges`
- improve coverage with tests for unsigned/integer dtype paths used by Rust-backed overlap kernels

# 1.1.9 (26.01.26)
- pandas dependency bound to v2. This is in response to pandas 3.0.0 being released, breaking our doctests.

# 1.1.8 (30.12.25)
- to_gtf and to_gff3: fix bug where 'phase' (gtf) and 'frame' (gff3) are erroneously added to attributes field

# 1.1.7 (16.12.25)
- window_ranges: fix sort order issue in  when using by (#98 and #105)
- window_ranges: added argument add_window_id, updated documentation

# 0.1.17 (21.06.25)
- various fixes

# 0.1.14 (09.06.25)
- keep original row sort order

# 0.1.12 (10.05.25)
- keep original row sort order

# 0.1.9 (05.05.25)
- Function to compute interval metrics

# 0.1.8 (05.05.25)
- Dispatch to rust functions on type
- Add spliced subsequence multiple, cumsum group, and map to global

# 0.1.6 (20.03.25)
- Make sequence name a variable in get_sequence and get_transcript_sequence

# 0.1.5 (20.03.25)
- Fix bug where count_overlaps returned series not pyranges
- Add rust backend as non-optional dependency

# 0.1.4 (17-18.03.25)
Rust backend

# 0.1.0
New pyranges version, redesigned from scratch.
