Source: https://gometry.monicz.dev/guide/grids/

# Grids & geocodes

Discrete global grids turn geometry into compact cell IDs for grouping and
candidate filtering before exact geometry.

gometry covers four grids in one API — [H3](https://h3geo.org/),
[S2](https://s2geometry.io/), [Geohash](https://en.wikipedia.org/wiki/Geohash),
and XYZ tiles — that other ecosystems split across separate packages:

- **H3** — Uber's hexagonal grid, addressed by *resolution* (0 coarse … 15 fine).
- **S2** — Google's quadrilateral grid on the sphere, addressed by *level*
  (0 coarse … 30 fine).
- **Geohash** — the base-32 lon/lat bisection code, addressed by
  *precision* (1 coarse … 12 fine).
- **XYZ tiles** — the slippy-map [Web Mercator](https://epsg.org/crs_3857/WGS-84-Pseudo-Mercator.html) grid, addressed by *zoom*
  (0 coarse … 29 fine).

All four share **one cell-array shape** and the same `cell_rule` semantics.
[`gm.Cell`](https://gometry.monicz.dev/api/grids/#gometry.Cell "Cell") is the structural protocol every cell type satisfies.

Cover factories materialize cell keys. A grid cover narrows the search; it is not
an exact geometry answer. Retain the source geometry and use an exact predicate to
refine those candidates into exact matches. The chosen `cell_rule`, rather than a
bbox prefilter, determines which cells are candidates.

## Cells from points

Point cells are constructed with [`gm.H3Cell(...)`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell "H3Cell") and
[`gm.h3_cells(...)`](https://gometry.monicz.dev/api/h3/#gometry.h3_cells "h3_cells") (likewise
[`gm.S2Cell(...)`](https://gometry.monicz.dev/api/s2/#gometry.S2Cell "S2Cell") / [`gm.s2_cells(...)`](https://gometry.monicz.dev/api/s2/#gometry.s2_cells "s2_cells")). The scalar
constructor returns a typed cell object; the plural `cells(...)` function returns a
typed [`CellArray`](https://gometry.monicz.dev/api/grids/#gometry.CellArray "CellArray") (one cell per coordinate).

```python
import gometry as gm

p = gm.Point(21.0, 52.0, crs=4326)

h = gm.H3Cell(p, resolution=8)
print("H3 cell:", h.id, "| resolution:", h.resolution)
print("parent (resolution 7):", h.parent(7).id, "| #children at resolution 9:", len(h.children(9)))

s = gm.S2Cell(p, level=14)
print("S2 cell token:", s.token, "| level:", s.level, "| id:", s.id)
print("parent (level 10) token:", s.parent(10).token)
```

```text
H3 cell: 613040634728546303 | resolution: 8
parent (resolution 7): 608537035103272959 | #children at resolution 9: 7
S2 cell token: 4719259f | level: 14 | id: 5123167415940677632
parent (level 10) token: 471925
```

The plural factories also accept an iterable of `Point` geometries or an
N×2-or-wider coordinate matrix. Point rows share one CRS and epoch just like a
`GeometryArray`; `None` rows remain missing cells. Bare coordinate matrices
are WGS84 longitude/latitude and use only their first two columns.

```python
import gometry as gm

points = [gm.Point(21.0, 52.0, crs=4326), None]
cells = gm.h3_cells(points, resolution=7)
print("missing rows:", cells.is_missing.tolist())
```

```text
missing rows: [False, True]
```

Cell arrays include grouping helpers for “count by cell” analytics:

```python
import gometry as gm

cells = gm.h3_cells([21.0, 21.0, 22.0], [52.0, 52.0, 52.5], resolution=7)
unique, counts = cells.value_counts()
print(list(zip(unique.token, counts.tolist())))

codes, uniques = cells.factorize()
print("codes:", codes.tolist(), "| uniques:", uniques.token)
```

```text
[('871f53591ffffff', 2), ('871f53bb3ffffff', 1)]
codes: [0, 0, 1] | uniques: ['871f53591ffffff', '871f53bb3ffffff']
```

Every cell class shares one uniform surface — [`.token`](https://gometry.monicz.dev/api/grids/#gometry.Cell.token "token property"), [`.center`](https://gometry.monicz.dev/api/grids/#gometry.Cell.center "center property"), [`.polygon`](https://gometry.monicz.dev/api/grids/#gometry.Cell.polygon "polygon property"),
[`.neighbors`](https://gometry.monicz.dev/api/grids/#gometry.Cell.neighbors "neighbors property"), [`.area`](https://gometry.monicz.dev/api/grids/#gometry.Cell.area "area property"), [`.parent(...)`](https://gometry.monicz.dev/api/grids/#gometry.Cell.parent "parent"), [`.children(...)`](https://gometry.monicz.dev/api/grids/#gometry.Cell.children "children"), and
cell-hierarchy [`.contains(...)`](https://gometry.monicz.dev/api/grids/#gometry.Cell.contains "contains") / [`.intersects(...)`](https://gometry.monicz.dev/api/grids/#gometry.Cell.intersects "intersects") — captured by the
`gm.Cell` protocol, so grid-system-agnostic
code type-checks against any of them.

Each cell class is constructed directly —
`H3Cell(value)` / `S2Cell(value)` / [`Tile(value)`](https://gometry.monicz.dev/api/tiles/#gometry.Tile "Tile") — accepting a cell object,
a numeric id (where the system has one), or a token string.

H3 topological
vertices and directed edges are separate scalar identities ([`H3Vertex(value)`](https://gometry.monicz.dev/api/h3/#gometry.H3Vertex "H3Vertex"),
[`H3Edge(value)`](https://gometry.monicz.dev/api/h3/#gometry.H3Edge "H3Edge")) with fixed arrays ([`H3VertexArray`](https://gometry.monicz.dev/api/h3/#gometry.H3VertexArray "H3VertexArray"), [`H3EdgeArray`](https://gometry.monicz.dev/api/h3/#gometry.H3EdgeArray "H3EdgeArray")) because
they do not have cell hierarchy or area/polygon operations.

Each system adds
its own vocabulary on top: [`H3Cell.resolution`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell.resolution "resolution property"),
[`.grid_disk(k)`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell.grid_disk "grid_disk") / [`.grid_ring(k)`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell.grid_ring "grid_ring") / [`.grid_path(...)`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell.grid_path "grid_path"); [`S2Cell.level`](https://gometry.monicz.dev/api/s2/#gometry.S2Cell.level "level property").
Arbitrary [`CellArray`](https://gometry.monicz.dev/api/grids/#gometry.CellArray "CellArray") sets roll up and down with
[`cells.compact()`](https://gometry.monicz.dev/api/grids/#gometry.CellArray.compact "compact") / [`cells.uncompact(depth)`](https://gometry.monicz.dev/api/grids/#gometry.CellArray.uncompact "uncompact").

For a tile coordinate, name its frame: use `Tile(lon=..., lat=..., zoom=...)`
for WGS84 coordinates or `Tile(x=..., y=..., zoom=...)` for XYZ indices.
`Tile(Point(...), zoom=...)` is the geometry form. A two-number positional call
is rejected because it cannot say which frame those numbers occupy.

Scalar cells keep native depth names; CellArray uses generic depth

H3 says `resolution`, S2 says `level`, geohash says `precision`, and XYZ
tiles say `zoom` — scalar cells keep those native names on constructors,
`parent`, and `children`. A `CellArray` may be generic over the cell kind,
so its hierarchy methods use one positional `depth` argument:
`cells.parent(5)`, `cells.compact(5)`, and `cells.uncompact(7)`.

`.center` and `.polygon` are properties; `parent`/`children` step by one by default

On both `H3Cell` and `S2Cell`, `.center` and `.polygon` are *properties* (no
parentheses) returning a WGS84 [`Geometry`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry "Geometry"). For H3 and
S2 it is a planar lon/lat chord proxy for display and interoperability, not
the spherical cell boundary. `parent()` defaults to one
step coarser and `children()` to one step finer; pass a target
(`cell.parent(resolution)`, `cell.children(level)`) to jump straight to it. A
maximum-depth cell yields an empty `CellArray` from `children()`; a depth-0 cell
raises on `parent()`.

Cell `.area` is reported in square meters using each grid's area model. H3 and
S2 use an authalic sphere; geohash and tile use the WGS 84 ellipsoidal area of
their longitude/latitude rectangle. H3/S2 `.polygon` values approximate curved
spherical boundaries with straight longitude/latitude edges, so their
`polygon.area` need not equal `cell.area`. For geohash and tile, `.polygon` and
`.area` share the rectangle definition but use separate binary64 area paths, so
their results are not guaranteed to be bit-identical.

### The geometry behind a cell ID

A cell ID is shorthand for an area on the ground. Scalar cells expose `.polygon`,
and [`CellArray.polygon`](https://gometry.monicz.dev/api/grids/#gometry.CellArray.polygon "polygon property readonly") returns a whole batch:

```python
import gometry as gm
cell = gm.H3Cell(gm.Point(21.0, 52.0, crs=4326), resolution=6)
poly = cell.polygon
print('cell', cell.id, '→', poly.geometry_type)
print('WKT:', poly.to_wkt()[:48], '...')
print('batch:', type(cell.grid_disk(1).polygon).__name__)
```

```text
cell 604033435576565759 → Polygon
WKT: POLYGON ((20.93798063515994 52.01937752721443, 2 ...
batch: GeometryArray
```

For H3 and S2 that polygon is a planar lon/lat chord proxy rather than the
spherical cell boundary. Adjacency and
local indexing are cell methods: `neighbors` lists the edge-adjacent ring,
[`is_neighbor`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell.is_neighbor "is_neighbor") tests one candidate, and [`local_ij`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell.local_ij "local_ij")/[`cell_from_local_ij`](https://gometry.monicz.dev/api/h3/#gometry.H3Cell.cell_from_local_ij "cell_from_local_ij") move through
the local grid-algebra space around an anchor. Resolution metadata lives in the
H3-prefixed global family — [`gm.h3_pentagons`](https://gometry.monicz.dev/api/h3/#gometry.h3_pentagons "h3_pentagons") and
[`gm.h3_base_cells`](https://gometry.monicz.dev/api/h3/#gometry.h3_base_cells "h3_base_cells"):

cell.children() nested inside one H3 cell

The seven child cells tile the interior of the single parent cell.

- input
- result

```python
import gometry as gm

cell = gm.H3Cell(gm.Point(21.0, 52.0, crs=4326), resolution=6)
neighbor = cell.neighbors[0]
print("adjacent:", cell.is_neighbor(neighbor), "| base cell:", cell.base_cell)
i, j = neighbor.local_ij(cell)
print("local ij:", (i, j), "→", cell.cell_from_local_ij(i, j) == neighbor)
print("pentagons at res 4:", len(gm.h3_pentagons(4)))
```

```text
adjacent: True | base cell: 15
local ij: (31, -45) → True
pentagons at res 4: 12
```

neighbors around one H3 cell

Six neighbor cells ring the centered cell, sharing one edge with it each.

- input
- result
- anchor

## The spherical model

S2 uses a spherical model for cells and coverings, so the antimeridian and poles
do not create a longitude seam in cell selection. gometry exposes typed cells and
cell sets rather than a general spherical boolean-topology engine.

```python
import gometry as gm

seam = gm.box(175.0, -5.0, -175.0, 5.0, crs=4326)
covering = gm.s2_cover(seam, max_cells=24)
positive_lon = sum(1 for cell in covering.polygon if cell.bounds[0] >= 0)

print("cells:", len(covering), "| levels:", sorted({cell.level for cell in covering}))
print("cells at 175°E..180°:", positive_lon, "| at 180°..175°W:", len(covering) - positive_lon)
print("covered / requested area:", round(covering.to_polygon().area / seam.area, 2))
```

```text
cells: 7 | levels: [4, 5]
cells at 175°E..180°: 2 | at 180°..175°W: 5
covered / requested area: 1.02
```

The covering spans both sides of the meridian and stays a single cell set.

For WGS 84 planar *geometry* crossing ±180 (as opposed to cells), see
[Across the antimeridian](https://gometry.monicz.dev/guide/crs/#across-the-antimeridian).

```python
import gometry as gm

p = gm.Point(179.9, 0.5, crs=4326)
cell = gm.S2Cell(p, level=12)
print("S2 cell level:", cell.level, "| token:", cell.token)
print("parent level 8:", cell.parent(8).token)

poly = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
covering = gm.s2_cover(poly, level=10, max_cells=None)
print("covering cells:", len(covering))
```

```text
S2 cell level: 12 | token: 65552ef
parent level 8: 65553
covering cells: 427
```

## Covering an area

[`gm.h3_cover(geom, resolution=...)`](https://gometry.monicz.dev/api/h3/#gometry.h3_cover "h3_cover"), fixed-level
[`gm.s2_cover(geom, level=...)`](https://gometry.monicz.dev/api/s2/#gometry.s2_cover "s2_cover"), and adaptive
[`gm.s2_cover(geom, target_cells=...)`](https://gometry.monicz.dev/api/s2/#gometry.s2_cover "s2_cover") calls return a typed
`CellArray` directly:

```python
import gometry as gm

area = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
cov = gm.h3_cover(area, resolution=5)

print("# cells:", len(cov))
print("first cell id:", cov[0].id)
```

```text
# cells: 159
first cell id: 599508794501758975
```

Cover functions also accept an iterable of geometries. They return one
`Groups` row per input item. A `None` input produces a missing row, while a
present empty geometry produces an empty `CellArray`.

```python
import gometry as gm

area = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
groups = gm.h3_cover([area, None], resolution=5)
print("rows:", len(groups), "| missing:", groups.is_missing.tolist())
print("missing row:", groups[1])
```

```text
rows: 2 | missing: [False, True]
missing row: None
```

### `cell_rule` is explicit semantics

The `cell_rule` argument selects which cells are included. The same four
modes apply to H3, S2, geohash, and tiles. It defaults to `"overlap"`, the
complete-coverage superset used for candidate keys.

| `cell_rule` | A cell is included when… | Typical use |
| --- | --- | --- |
| `"within"` | the cell lies **entirely inside** the geometry | a *subset* — cells the area fully owns |
| `"center"` | the cell's **center** falls inside the geometry | h3-py-style polygon-to-cell selection; boundary cells may be omitted |
| `"overlap"` (default) | the cell **intersects** the geometry at any point (interior or boundary) | a *superset* — safe join/prefilter keys, never misses a true hit |
| `"bbox"` | the cell meets the geometry's **envelope** (one window per part, split at the antimeridian) | loosest and fastest candidate rule: no shape test decides it |

```python
import gometry as gm

area = gm.Polygon(
    [(20.0, 51.25), (20.25, 51.0), (22.0, 52.75), (21.75, 53.0), (20.0, 51.25)],
    crs=4326,
)
for rule in ("within", "center", "overlap", "bbox"):
    cov = gm.h3_cover(area, resolution=5, cell_rule=rule)
    if rule == 'within':
        assert all(gm.covers(area, cell.polygon) for cell in cov)
    elif rule == 'center':
        assert all(gm.covers(area, cell.center) for cell in cov)
    elif rule == 'overlap':
        assert all(gm.intersects(area, cell.polygon) for cell in cov)
    elif rule == 'bbox':
        assert all(gm.intersects(area.envelope(), cell.polygon) for cell in cov)
    print(f"{rule:>8}: {len(cov):>3} cells")
```

```text
  within:  12 cells
  center:  30 cells
 overlap:  46 cells
    bbox: 159 cells
```

For this rotated rectangle and resolution, the selected cell sets form these
nested sets: `"within"` ⊆ `"center"` ⊆ `"overlap"` ⊆ `"bbox"`.

cell\_rule='within'

Every selected cell lies wholly inside the polygon, so the cells stop short of its edges.

cell\_rule='center'

Every selected cell has its center inside the polygon, so the cells reach the edges and overhang them slightly.

cell\_rule='overlap'

Every cell touching the polygon is selected, so the cells cover it completely and spill past every edge.

cell\_rule='bbox'

Every cell meeting the polygon envelope is selected, filling the whole bounding rectangle around the diagonal shape.

- candidates
- input

Each rule keeps the one before it and adds the cells its looser test admits.

The rotated rectangle has a much wider envelope than its area, so each panel
adds cells for a distinct reason: the cell is inside the shape, its center is
inside, the cell intersects the shape, or the cell reaches the envelope.

The returned cell set depends on `cell_rule`; exact geometry predicates still use
the source geometry.

### Input is normalized to WGS 84 lon/lat

All four grid factories accept a **WGS 84 lon/lat interchange domain** (and
transform supported projected input into that domain first). The grids themselves
are not one model: H3 is an icosahedral discrete global grid, S2 a spherical
cube, geohash a lon/lat rectangle hierarchy, and tiles the Web Mercator XYZ
scheme.

If you hand `gm.h3_cover` / `gm.s2_cover` a geometry in a supported
projected CRS, gometry transforms it to WGS 84 lon/lat first. WGS 84 and CRS-free
lon/lat pass through; other supported frames are reprojected before cover keys
are generated.

This normalization is a grid-factory boundary: do not pass native
radian or grad values as bare WGS 84 coordinates. Attach the source CRS to a
geometry and let the factory transform it first.

```python
import gometry as gm
area = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
projected = area.to_crs(3857)
cov = gm.h3_cover(projected, resolution=5)
print('# cells:', len(cov))
print('projected candidate:', gm.covers(projected, gm.Point(21.0, 52.0, crs=4326).to_crs(3857)))
```

```text
# cells: 159
projected candidate: True
```

## Exact membership with the source geometry

The returned `CellArray` contains the cell keys selected by `cell_rule`; it does
not retain the source geometry or provide exact source-membership results. Keep
the source geometry and apply [`gm.covers`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.covers "covers"), [`gm.contains`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.contains "contains"), or [`gm.intersects`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.intersects "intersects") to
determine exact membership for candidate rows.

```python
import gometry as gm

area = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
points = gm.points([21.0, 22.1, 30.0], [52.0, 52.0, 52.0], crs=4326)
cover_tokens = {cell.token for cell in gm.h3_cover(area, resolution=5)}
point_cells = gm.h3_cells(points.coords.x, points.coords.y, resolution=5)
candidate_rows = [i for i, cell in enumerate(point_cells) if cell.token in cover_tokens]
exact_rows = [i for i, point in enumerate(points) if gm.covers(area, point)]

print("candidate rows:", candidate_rows)
print("exact covered rows:", exact_rows)
assert candidate_rows == [0, 1]
assert exact_rows == [0]
```

```text
candidate rows: [0, 1]
exact covered rows: [0]
```

The H3 key join admits row 1 because its cell overlaps the source cover; the
source-geometry predicate removes that candidate. The cell key is a candidate,
not an exact point-in-polygon result.

```python
import gometry as gm
area = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
cells = gm.h3_cover(area, resolution=5)
points = gm.points([21.0, 30.0], [52.0, 52.0], crs=4326)
print('covers:', repr(gm.covers(area, points)))
print('edge point:', gm.covers(area, gm.Point(20.0, 52.0, crs=4326)), '| strictly inside:', gm.contains(area, gm.Point(20.0, 52.0, crs=4326)))
print('raw lon/lat stream:', repr(gm.intersects(area, x=[21.0, 30.0], y=[52.0, 52.0])))
```

```text
covers: array([ True, False])
edge point: True | strictly inside: False
raw lon/lat stream: array([ True, False])
```

H3 overlap cells are candidate join keys, not membership answers.

H3 overlap cells can extend beyond the source area, so exact membership still uses the source geometry.

- candidates
- input

Joins at scale

Use the default `"overlap"` cells as join keys (a superset never loses a
candidate), join on cell IDs in your database or stream, then finish with
`gm.covers(area, matched)` for the exact answer. For in-memory many-to-many joins,
[`gm.join`](https://gometry.monicz.dev/api/toplevel/index-join/#gometry.join "join") does both stages for you.

## From cells back to geometry

A cell ID is shorthand for a polygon, and a covering is a mesh of them. A
single-cell bound or a dissolved covering recovers geometry.

### The single cell that bounds a geometry

A covering returns *many* cells. A bounding-cell operation returns the **one** cell
that wholly contains a geometry, which serves as a coarse partition key or spatial
bucket.
Each grid spells it by return type: [`gm.h3_bounding_cell`](https://gometry.monicz.dev/api/h3/#gometry.h3_bounding_cell "h3_bounding_cell") / [`gm.s2_bounding_cell`](https://gometry.monicz.dev/api/s2/#gometry.s2_bounding_cell "s2_bounding_cell")
/ [`gm.geohash_bounding_cell`](https://gometry.monicz.dev/api/geohash/#gometry.geohash_bounding_cell "geohash_bounding_cell") return their cell type, and [`gm.tile_bounding_cell`](https://gometry.monicz.dev/api/tiles/#gometry.tile_bounding_cell "tile_bounding_cell")
returns a `Tile`.

```python
import gometry as gm

berlin = gm.box(13.0, 52.3, 13.7, 52.7, crs=4326)

h3_cell = gm.h3_bounding_cell(berlin)
s2_cell = gm.s2_bounding_cell(berlin)
print("h3:", h3_cell, "res", h3_cell.resolution)
print("s2:", s2_cell, "level", s2_cell.level)
print("geohash:", gm.geohash_bounding_cell(berlin).token)
print("tile:", gm.tile_bounding_cell(berlin))
```

```text
h3: 821f1ffffffffff res 2
s2: 47a9 level 6
geohash: u33
tile: 12021
```

Each returns the *deepest* single cell whose certified extent covers the complete
lon/lat bounding region, not just the input vertices. The proof includes region
interiors and curved cell-edge bulges; seam and pole extents are normalized
before checking, and polygon holes do not shrink the bounding region. A
candidate that cannot be proved is replaced by a coarser proven ancestor. Pass
a geometry, an array, or a raw `(minx, miny, maxx, maxy)` bounds tuple.

### Dissolving a covering back to one outline

To recover one geometry from a covering, [`to_polygon()`](https://gometry.monicz.dev/api/grids/#gometry.CellArray.to_polygon "to_polygon") dissolves it into one
outline; shared cell edges cancel, so the interior costs no geometry:

```python
import gometry as gm

cov = gm.h3_cover(gm.box(20.0, 51.0, 22.0, 53.0, crs=4326), resolution=5)
print("dissolved:", cov.to_polygon().geometry_type)
```

```text
dissolved: Polygon
```

The outline uses WGS 84 longitude/latitude coordinates. When the selected cells cross the
antimeridian, it preserves the occupied regions on both sides of ±180° rather
than filling the longitude span through the middle of the map; the chart-cut
edge is not part of the represented cell boundary.

When you have a *detached* list of cell IDs rather than a coverage — rows from a
database, the output of `compact`/`union`, a hand-built set — build a
`CellArray` and call `cells.to_polygon()` (mixed resolutions allowed, so
compacted sets work as-is):

```python
import gometry as gm

area = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
cells = gm.h3_cover(area, resolution=6).compact()
print(cells.to_polygon().geometry_type, "from", len(cells), "compacted cells")
```

```text
Polygon from 180 compacted cells
```

Floor-bounded compaction keeps detail where you ask it to stop:
`cells.compact(4)` for H3 resolution 4, `cells.compact(9)` for S2 level 9,
`cells.compact(5)` for geohash precision 5, and `cells.compact(8)` for tile
zoom 8.

`cov.to_polygon()` is not `coverage_*`

`to_polygon()` dissolves *grid cells* — an approximate outline of the
source under the grid. Polygonal-coverage operations work on source
polygons: [`coverage_union_all`](https://gometry.monicz.dev/api/toplevel/coverage-polygonal/#gometry.coverage_union_all "coverage_union_all") and
[`coverage_simplify`](https://gometry.monicz.dev/api/toplevel/coverage-polygonal/#gometry.coverage_simplify "coverage_simplify") require a valid coverage;
[`coverage_clean`](https://gometry.monicz.dev/api/toplevel/coverage-polygonal/#gometry.coverage_clean "coverage_clean") rebuilds a near-coverage. See
[choosing a coverage operation](https://gometry.monicz.dev/guide/choosing/).

## Cell-set algebra (all four grids)

Every grid family exposes the same **factories + set-algebra** shape: prefixed
`union`, `intersection`, and `difference` functions on cell sets (H3, S2,
geohash, and tiles). Hierarchy behavior still differs per system (S2 range nesting,
H3 resolution parents, geohash/tile prefix parents). H3 cell sets require
hierarchy-aware free functions rather than plain Python `set` arithmetic.

**H3 resolution normalization** ([`gm.h3_union`](https://gometry.monicz.dev/api/h3/#gometry.h3_union "h3_union") / [`gm.h3_intersection`](https://gometry.monicz.dev/api/h3/#gometry.h3_intersection "h3_intersection") /
[`gm.h3_difference`](https://gometry.monicz.dev/api/h3/#gometry.h3_difference "h3_difference")): inputs may mix resolutions. The result is cell-ID algebra
under the compact contract — sorted, with descendants absorbed by ancestors and
complete sibling groups merged into parents — so mixed-resolution operands are
normalized rather than rejected. This is identity algebra (an H3 child's *geometry*
does not nest exactly inside its parent, but its id does). S2 algebra is
range-nesting exact; geohash/tile algebra is prefix-based.

Every set-algebra, `compact()`, and `uncompact()` result uses the grid family's
public identity order: H3 and S2 numeric id order, tile packed Morton-id order,
and geohash token order.
Set-algebra operands skip `None` entries inside an iterable; use an empty
iterable to represent an empty set.

```python
import gometry as gm

a = gm.h3_cover(gm.box(20.0, 51.0, 22.0, 53.0, crs=4326), resolution=5)
b = gm.h3_cover(gm.box(21.0, 52.0, 23.0, 54.0, crs=4326), resolution=5)
print("h3 intersection:", len(gm.h3_intersection(a, b)))
mixed = gm.h3_union(a, gm.h3_cover(gm.box(21.0, 52.0, 23.0, 54.0, crs=4326), resolution=6))
print("h3 mixed-res union resolutions:", sorted({c.resolution for c in mixed}))
print("s2 union sample:", len(gm.s2_union(
    gm.s2_cover(gm.box(20.0, 51.0, 21.0, 52.0, crs=4326), level=8),
    gm.s2_cover(gm.box(21.0, 52.0, 22.0, 53.0, crs=4326), level=8),
)))
```

```text
h3 intersection: 27
h3 mixed-res union resolutions: [3, 4, 5, 6]
s2 union sample: 17
```

## S2 coverings: fixed levels and adaptive budgets

S2 coverings are multi-resolution, and S2 cell IDs nest as ranges. These
properties provide a `cover` budget and whole-set algebra without touching
geometry.

### A budget and a rule

`gm.s2_cover(geom, level=...)` materializes cells only at the requested level.
It never coarsens to fit `max_cells`. Fixed and adaptive modes use the same
four `cell_rule` values:

```python
import gometry as gm
area = gm.box(20.0, 51.0, 22.0, 53.0, crs=4326)
cov = gm.s2_cover(area, level=8, max_cells=64)
print('# cells:', len(cov))
print('levels present:', sorted({c.level for c in cov}))
```

```text
# cells: 36
levels present: [8]
```

Omit `level` to use adaptive refinement through the same
[`gm.s2_cover`](https://gometry.monicz.dev/api/s2/#gometry.s2_cover "s2_cover") operation. `target_cells`, `min_level`,
`max_level`, and `level_mod` control that mode. The target guides refinement
but is not a hard limit; the legal minimum cover can exceed it. Refinement may
retain coarser cells within the requested level range when children would
exceed `max_cells`. A fixed `level` cannot be combined with non-default
adaptive controls.

Both S2 modes default `max_cells` to `1_000_000`; grouped calls apply it to the
total returned cells. Discarded candidates do not consume the cap. Pass
`max_cells=None` for an unlimited cover (bounded only by memory). The cap does
not bound classifier or traversal CPU work. The cells are spatial keys
materialized according to `cell_rule`; exact geometry predicates remain free
functions such as `gm.covers` and [`gm.contains`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.contains "contains") with its
coordinate-keyword form.

### Cell-set algebra

S2 cell ids nest as ranges, so whole-set operations stay exact without touching
geometry: [`gm.s2_union`](https://gometry.monicz.dev/api/s2/#gometry.s2_union "s2_union") absorbs descendants and merges complete
sibling groups, [`gm.s2_intersection`](https://gometry.monicz.dev/api/s2/#gometry.s2_intersection "s2_intersection") keeps the finer cell
of an ancestor/descendant overlap, and [`gm.s2_difference`](https://gometry.monicz.dev/api/s2/#gometry.s2_difference "s2_difference")
splits partially-covered cells exactly (`gm.S2Cell` takes a [`Point`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point "Point") or a
bare `lon, lat` pair):

```python
import gometry as gm
cell = gm.S2Cell(13.4, 52.5, level=10)
children = cell.children()
print('union:', list(gm.s2_union(children[:2], children[2:])) == [cell])
print('intersection:', [c.level for c in gm.s2_intersection([cell], children[:2])])
print('difference:', len(gm.s2_difference([cell], children[:1])), 'cells remain')
```

```text
union: True
intersection: [11, 11]
difference: 3 cells remain
```

`gm.h3_union` / `gm.h3_intersection`
/ `gm.h3_difference` normalize mixed resolutions under
the compact contract. A fixed-resolution `set` of raw IDs performs identity
equality without parent/child absorption.

## Geohash and XYZ tiles

Geohash and tiles are *rectangular* grids — every cell is an exact lon/lat
rectangle, and their cover keys use those rectangles rather than a separate
bounding-box approximation. They wear the same API as H3 and S2.

Tile point ownership is representational: geographic construction uses the
same emitted binary64 edges as the tile footprint. Longitude is
west-inclusive/east-exclusive, and latitude is north-inclusive/south-exclusive
at shared edges. This defines stable cell IDs for stored floating-point
coordinates; it is not a claim that the ideal Web Mercator transcendental
function is exactly rounded in binary64.

```python
import gometry as gm
point = gm.Point(13.4, 52.5, crs=4326)
print('geohash:', gm.GeohashCell(point, precision=6).token)
print('tile:', gm.Tile(point, zoom=12).token)
area = gm.box(13.2, 52.4, 13.6, 52.6, crs=4326)
cov = gm.geohash_cover(area, precision=5, cell_rule='overlap')
print('# geohash cells:', len(cov))
print('covers a point:', gm.covers(area, gm.Point(13.4, 52.5, crs=4326)))
tiles = gm.tile_cover(area, zoom=10)
print('# tiles:', len(tiles), 'first quadkey:', tiles[0].token)
```

```text
geohash: u33d8v
tile: 120210233222
# geohash cells: 50
covers a point: True
# tiles: 4 first quadkey: 1202102323
```

A geohash or tile cover is built as a **uniform single-depth tiling** —
every cell is the same precision/zoom, which makes the returned `CellArray` a
uniform spatial key. Exact predicates test the source geometry supplied by the caller;
token lookup alone is not the answer. Cell-set methods operate on the returned
`CellArray` directly.

[`GeohashCell`](https://gometry.monicz.dev/api/geohash/#gometry.GeohashCell "GeohashCell") and `Tile` carry the full cell surface — `center`,
`polygon`, `area`, `parent`/`children`/`neighbors`, ordering, and pickle —
and `CellArray` adds the set operations (`cells.compact()` /
`cells.uncompact(depth)`) while H3 vertex/edge arrays expose only their valid
topological surfaces. `Tile` keeps the quadkey constructor
(`gm.Tile('0313102310')`). Tiles reject latitudes outside the square Web Mercator
tile grid (±85.0511°), a representational extent distinct from the finite
projection domain; they do not clamp coordinates.

## Aggregate by cell, then refine

Cell arrays provide global bucketing, while a spatial index provides exact
polygon refinement. The grid step groups rows before the geometry predicate.

```python
import gometry as gm

lons = [2.3479, 2.3490, 2.3600, 2.3470]
lats = [48.8589, 48.8590, 48.8650, 48.8580]
points = gm.points(lons, lats, crs=4326)

h3_cells = gm.h3_cells(lons, lats, resolution=8)
unique_h3, counts = h3_cells.value_counts()
codes, unique_for_codes = h3_cells.factorize()

s2_cells = gm.s2_cells(lons, lats, level=12)
unique_s2, _ = s2_cells.value_counts()

districts = gm.GeometryArray([
    gm.box(2.34, 48.85, 2.35, 48.86, crs=4326),
    gm.box(2.35, 48.86, 2.37, 48.87, crs=4326),
])
point_to_district = gm.SpatialIndex(districts).query(points, predicate="within")

print("H3 counts:", list(zip(unique_h3.token, counts.tolist())))
print("H3 codes:", codes.tolist(), "unique rows:", len(unique_for_codes))
print("unique S2 cells:", len(unique_s2))
print("refined district rows:", point_to_district.values.tolist())
```

```text
H3 counts: [('881fb4662dfffff', 3), ('881fb46629fffff', 1)]
H3 codes: [0, 0, 1, 0] unique rows: 2
unique S2 cells: 1
refined district rows: [0, 0, 1, 0]
```

[`value_counts()`](https://gometry.monicz.dev/api/grids/#gometry.CellArray.value_counts "value_counts") is the direct aggregation primitive. [`factorize()`](https://gometry.monicz.dev/api/grids/#gometry.CellArray.factorize "factorize") is the
handoff shape for pandas/NumPy-style grouping: `codes[i]` points at a row in the
returned unique `CellArray`.

Cell equality is a candidate join, not polygon truth. After exporting cells
to a DataFrame or lakehouse, refine matched rows with [`SpatialIndex.query`](https://gometry.monicz.dev/api/spatial-index/#gometry.SpatialIndex.query "query"),
`gm.join`, or the corresponding exact predicate against the original geometry.

## Ordering & locality

A [space-filling curve](https://en.wikipedia.org/wiki/Hilbert_curve) threads a
single line through a 2-D grid. Encoding each geometry as its position along that
curve produces an **integer key whose sorted blocks tend to cluster nearby
locations** for [GeoParquet](https://geoparquet.org/) row-group packing, spatial
shuffles, and index builds. Numeric key distance is not geographic distance:
every curve has discontinuities, so use bounds or exact predicates for spatial
decisions.

gometry exposes one keying and sorting API with an explicit `curve=` choice.
S2 and tile cell identifiers also encode locality-preserving orderings:

- **Hilbert** — the default `curve='hilbert'`, a recursive locality-preserving
  order.
- **Morton (Z-order)** — `curve='morton'`. Cheaper to compute (bit
  interleave), with one diagonal jump per quadrant — the [Z-order curve](https://en.wikipedia.org/wiki/Z-order_curve).

### Keys from geometries

[`spatial_key`](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray.spatial_key "spatial_key") discretizes each row's
bounding-box center onto a `2**level × 2**level` grid over one shared frame, the
array's [`total_bounds`](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray.total_bounds "total_bounds property") unless `bounds=` is
given, and returns its position along the selected curve. Keys compare only
within one frame, so pass the same `bounds=` when keying separate arrays.
Sorting keys tends to group nearby locations into blocks, but numerically close
keys can be geographically distant at curve discontinuities.

```python
import gometry as gm
cities = gm.GeometryArray([gm.Point(-0.13, 51.51), gm.Point(2.35, 48.86), gm.Point(-73.94, 40.72), gm.Point(-74.01, 40.71)])
keys = cities.spatial_key(level=16)
print(repr(keys))
```

```text
array([2859112873, 2952549291,       2720,          0], dtype=uint64)
```

The default `level=16` is **gometry's default only**. GeoParquet does not define a
Hilbert level or a canonical key algorithm. Bit-identical ordering across tools
requires matching bounds, level, quantization, coordinate convention, and
algorithm. Another writer may use different defaults.

### Sorting an array

[`sort_by_spatial_key`](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray.sort_by_spatial_key "sort_by_spatial_key") returns the
array reordered by the selected key, so sorted blocks tend to hold nearby
geometries — useful packing for row-group min/max filters and block reads. It is
not a spatial predicate or a distance order.

```python
import gometry as gm

grid = gm.GeometryArray([gm.Point(x, y) for y in range(4) for x in range(4)])
ordered = grid.sort_by_spatial_key(level=8)
print([(round(p.x), round(p.y)) for p in ordered])
```

```text
[(0, 0), (1, 0), (1, 1), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 2), (2, 3), (3, 3), (3, 2), (3, 1), (2, 1), (2, 0), (3, 0)]
```

sort\_by\_spatial\_key()

sort\_by\_spatial\_key()

sort\_by\_spatial\_key(curve='morton')

sort\_by\_spatial\_key(curve='morton')

The sorted order can be written to GeoParquet or passed to [`gm.SpatialIndex`](https://gometry.monicz.dev/api/spatial-index/#gometry.SpatialIndex "SpatialIndex"); each
block then contains a spatially compact run of geometries.

### The grids are curves too

S2, tile, and geohash identifiers encode these orderings, so sorting their keys
can provide the same block-locality benefit. It does not make numeric differences
geographic distances or remove discontinuities:

- **[S2](https://s2geometry.io/) cell ids are Hilbert order.** `sorted(s2_cells)`
  tends to group spatial neighbors because an S2 id is the cell's position along
  a face Hilbert curve. The cell-set algebra relies on its range hierarchy.
- **Tile ids and [quadkeys](https://learn.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system) are Morton order.** A `Tile`'s packed id interleaves
  [`x`](https://gometry.monicz.dev/api/tiles/#gometry.Tile.x "x property")/[`y`](https://gometry.monicz.dev/api/tiles/#gometry.Tile.y "y property") so it sorts in quadkey order; [`tile.morton`](https://gometry.monicz.dev/api/tiles/#gometry.Tile.morton "morton property") is that index directly.
- **[Geohash](https://en.wikipedia.org/wiki/Geohash) token order is a Z-ish curve.**
  Base-32 tokens sort lexicographically into an order that tends to preserve
  locality, which is why a `GeohashCell` token carries that ordering directly.

Geometry arrays use `spatial_key()` for GeoParquet or index packing, with
`curve='morton'` selecting Morton order. Cell identities carry their grid's
ordering directly.

## Point geocodes

[Open Location Code](https://github.com/google/open-location-code) (plus codes)
and OSM shortlinks are compact point codes rather than cell systems. gometry
exposes them as direct, codec-named functions:

```python
import gometry as gm

code = gm.pluscode_encode(8.628, 47.366)
print("plus code:", code)
print("cell:", gm.pluscode_polygon(code).bounds)

link = gm.osm_shortlink_encode(13.365, 52.5077, zoom=17)
print("shortlink:", link)
print("decoded:", gm.osm_shortlink_location(link))
```

```text
plus code: 8FVC9J8H+C6
cell: (8.628, 47.366, 8.628125, 47.366125)
shortlink: 0MbEUxVoG-
decoded: (13.36499959230423, 52.50769883394241, 17)
```

`pluscode_encode` accepts a `Point` or `GeometryArray` (CRS-aware), as well as
coordinate columns. [`pluscode_shorten`](https://gometry.monicz.dev/api/geocode/#gometry.pluscode_shorten "pluscode_shorten") and
[`pluscode_recover`](https://gometry.monicz.dev/api/geocode/#gometry.pluscode_recover "pluscode_recover") accept a `Point` reference or
broadcast longitude and latitude columns; one scalar code is applied to each
coordinate row. [`osm_shortlink_location`](https://gometry.monicz.dev/api/geocode/#gometry.osm_shortlink_location "osm_shortlink_location")
accepts the legacy `@` spelling.

Coming from h3-py or s2sphere? See [Migrating](https://gometry.monicz.dev/migrating/#coming-from-h3-py-s2sphere).

## See also

- [Spatial indexing](https://gometry.monicz.dev/guide/indexing/) — candidate vs exact refine on geometry, and
  rebuilding an index after a spatial sort.
- [Arrow & storage](https://gometry.monicz.dev/ecosystem/arrow/) — exporting cell IDs and geohash tokens
  as Arrow columns without retaining grid-kind metadata.
- [The mental model](https://gometry.monicz.dev/get-started/mental-model/) — the three operation models.
- [API: h3\_cover](https://gometry.monicz.dev/api/h3/#gometry.h3_cover "h3_cover") · [s2\_cover](https://gometry.monicz.dev/api/s2/#gometry.s2_cover "s2_cover") ·
  [H3Cell](https://gometry.monicz.dev/api/h3/#gometry.H3Cell "H3Cell") · [S2Cell](https://gometry.monicz.dev/api/s2/#gometry.S2Cell "S2Cell") ·
  [SpatialIndex](https://gometry.monicz.dev/api/spatial-index/#gometry.SpatialIndex "SpatialIndex") ·
  [GeometryArray.spatial\_key](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray.spatial_key "spatial_key") ·
  [GeometryArray.sort\_by\_spatial\_key](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray.sort_by_spatial_key "sort_by_spatial_key")
