Skip to content

Range Queries

A Yoro code is not only a label for a place — it is a sortable key. That makes "what is near me" answerable with a plain BETWEEN on an ordinary B-tree index, with no spatial extension, no R-tree, and no PostGIS.

Why it works

Two properties line up:

  1. The alphabet is sorted. 23456789ABCDEFGHJKMNPQRSTVWXY is in increasing value order, so for bodies of equal length, lexicographic order on the text equals numeric order on the Hilbert index.
  2. The Hilbert curve is contiguous. Any aligned s × s block of cells occupies one unbroken run of s² indices.

So a rectangle on the ground becomes a handful of index intervals — and, thanks to (1), a handful of string intervals your database already knows how to seek.

ranges()

from yoro import ranges

for code_min, code_max in ranges(12.60, 12.66, -8.03, -7.97, precision=14, domain="ML"):
    cur.execute(
        "SELECT * FROM pois WHERE code BETWEEN ? AND ? ORDER BY code",
        (code_min, code_max),
    )

Both bounds are inclusive. The intervals are disjoint and sorted.

Compared to cells_in_bounds()

cells_in_bounds() enumerates every cell and refuses past max_cells, because the count grows with the area. ranges() returns the intervals containing those cells, so its output stays small however large the box. On one Bamako neighbourhood:

Precision cells_in_bounds() ranges()
~12 270 cells 18 intervals
~14 refuses (3886 > 2000) 49 intervals

Use cells_in_bounds() when you want to draw the grid; use ranges() when you want to query it.

Capping the interval count

Each interval costs one index seek. When you would rather do fewer seeks and filter afterwards, max_ranges merges the closest intervals:

ranges(12.60, 12.66, -8.03, -7.97, precision=14, domain="ML", max_ranges=4)

The result then covers some cells outside the box — merging spans the gaps — so filter on exact coordinates afterwards if precision matters. Nothing inside the box is ever dropped.

Truncated codes: prefix_range()

Dropping characters off the end of a code has two readings, and they are not the same region:

  • (a) the index interval [d · 29ᵗ, (d+1) · 29ᵗ) — a connected segment of the curve, exactly what LIKE 'prefix%' matches;
  • (b) decoding the prefix as a shorter code — a cell at the canonical precision of k − t, whose edges do not line up with the finer grid.

prefix_range() implements (a), and clips to the valid index range so it never spans codes decode() would reject:

from yoro import prefix_range

lo, hi = prefix_range("ML-4V7D", full_length=6)
cur.execute("SELECT * FROM places WHERE code BETWEEN ? AND ?", (lo, hi))

Prefer this over LIKE 'ML-4V7D%': it uses the same index as every other query and cannot accidentally match a code of a different length.

What this does not give you

Codes sort by Hilbert index, and a curve visits a plane in one dimension — so two cells that are neighbours on the ground can sit far apart in code order. That is why a box needs several intervals rather than one, and why the count grows with how badly the box straddles the curve's larger blocks. It is a locality heuristic that pays off, not a promise of contiguity.

The intervals also describe cells, not distances. ranges() gives you the candidates; if you need "within 500 m", filter the rows it returns.