You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
GBIF and OBIS hex: h0 is not the H3 parent of the native cell (8.7% / 7.7% of rows); cng-datasets builds are exact #697
Revised 2026-09-20. First filed as GBIF-only ("every other dataset is exact"). A survey
found OBIS shares it, and a control isolated the real discriminator — it is not point vs
polygon geometry, it is how the pipeline derives the parent columns. Title and scope updated.
Summary
In two hand-written occurrence pipelines, the h0 hive partition key is not the H3 parent
of the row's native cell. Everything cng-datasets builds is exact.
collection
built by
native
rows
cell_to_parent(native,0) <> h0
gbif-derived 2026-09
bespoke script
h10
3,570,154,958
8.713%
obis-derived 2026-09-09
bespoke script
h8
228,567,957
7.675%
inaturalist-ranges
cng-datasets
h4
377,963,890
0
unep-wcmc-coral-reefs-points
cng-datasets
h8
925
0
(GBIF's densest single h0 reads 21.754% — the rate varies strongly by geography.)
The coral-points row is the control that settles the cause. It is the same shape as GBIF
and OBIS — point geometry, native resolution 8, h0 as partition key — and it is exact. So
the discriminator is not geometry type and not point-ness; it is that cng-datasets derives
parents with cell_to_parent from a single native cell, while the two occurrence pipelines
recompute every resolution independently from the coordinate:
-- catalog/gbif/*/process_gbif_h3.py
h3_latlng_to_cell(decimallatitude, decimallongitude, 0) AS h0,
h3_latlng_to_cell(decimallatitude, decimallongitude, 1) AS h1,
...
H3's parent relation is combinatorial, not geometric: an aperture-7 hex grid does not nest
exactly, so a res-8 cell's area straddles several res-7 cells. A point near a boundary
therefore lands in a res-0 cell that is not cellToParent of its res-8 cell. Every h_k value
here is individually the correct cell for that point — this is correct H3 behaviour being used
in a way that breaks an invariant the rest of the catalog holds.
inaturalist-ranges is the positive control for the other direction: cell_to_parent(h4,2) <> h2
is 0 of 378 M, so cng-datasets' whole derived chain is internally consistent.
⚠️ What is NOT broken — read this before changing any query
h8 = h8 joins are exact and correct, including GBIF↔OBIS. Both pipelines compute h8
from the same lat/lng with the same call, so cells agree. The obis-derived description
advertises h8 as the resolution at which OBIS and GBIF can be compared, and that comparison
is sound. Using h8 directly as the join key is what AGENTS.md prescribes and what the
catalog actually does.
Only deriving a coarser cell via cell_to_parent breaks. A reader who takes away "the H3
columns are unreliable" will drop a join that is correct.
Why it matters
Partition pruning by derived parent silently under-reads.catalog/gbif/README.md sells
"Partition pruning: h0 partitioning eliminates entire directories from query". A consumer
holding an h8 and pruning to h0 = h3_cell_to_parent(h8,0) misses ~8.7% of GBIF rows and
~7.7% of OBIS rows — and 0% in every cng-datasets collection. That asymmetry is the
trap: a pattern validated on WDPA, IUCN or coral-reefs transfers to GBIF/OBIS and returns
quietly incomplete results, with no error raised.
OBIS is the more exposed of the two. Its h3:parent_resolutions is [0], so h0 is purely a partition key with no intermediate resolution to absorb the discrepancy. GBIF at
least carries h1–h10.
Hierarchical rollup disagrees with itself in the affected collections: GROUP BY h5 then
aggregating to h2 via cell_to_parent does not match a direct GROUP BY h2.
Options
Document it (cheapest): state the semantics in the GBIF and OBIS hex asset descriptions,
warn against deriving the partition key via cell_to_parent, and say explicitly that h8
joins are unaffected. Does not fix rollup.
Change the build to derive non-native resolutions with cell_to_parent from a single
native cell, matching the rest of the catalog. Makes all resolutions mutually consistent and
pruning-safe, at the cost of each h_k no longer being the exact cell containing the point at
resolution k. That is a real semantic trade, not a pure fix — decide it deliberately.
Either way, verify-stac.py could assert the parent-chain property so a future bespoke
pipeline does not reintroduce it silently.
Provenance
Surfaced from #695 (which needs only that its grouping key be a prefix of the sort key, and is
unaffected by this). Raised by a parallel audit session; the survey above was contributed there
and every figure was re-measured independently here via the duckdb-geo MCP.
Summary
In two hand-written occurrence pipelines, the
h0hive partition key is not the H3 parentof the row's native cell. Everything
cng-datasetsbuilds is exact.cell_to_parent(native,0) <> h0gbif-derived2026-09obis-derived2026-09-09inaturalist-rangesunep-wcmc-coral-reefs-points(GBIF's densest single h0 reads 21.754% — the rate varies strongly by geography.)
The coral-points row is the control that settles the cause. It is the same shape as GBIF
and OBIS — point geometry, native resolution 8,
h0as partition key — and it is exact. Sothe discriminator is not geometry type and not point-ness; it is that
cng-datasetsderivesparents with
cell_to_parentfrom a single native cell, while the two occurrence pipelinesrecompute every resolution independently from the coordinate:
H3's parent relation is combinatorial, not geometric: an aperture-7 hex grid does not nest
exactly, so a res-8 cell's area straddles several res-7 cells. A point near a boundary
therefore lands in a res-0 cell that is not
cellToParentof its res-8 cell. Everyh_kvaluehere is individually the correct cell for that point — this is correct H3 behaviour being used
in a way that breaks an invariant the rest of the catalog holds.
inaturalist-rangesis the positive control for the other direction:cell_to_parent(h4,2) <> h2is 0 of 378 M, so cng-datasets' whole derived chain is internally consistent.
h8 = h8joins are exact and correct, including GBIF↔OBIS. Both pipelines computeh8from the same lat/lng with the same call, so cells agree. The
obis-deriveddescriptionadvertises h8 as the resolution at which OBIS and GBIF can be compared, and that comparison
is sound. Using
h8directly as the join key is what AGENTS.md prescribes and what thecatalog actually does.
Only deriving a coarser cell via
cell_to_parentbreaks. A reader who takes away "the H3columns are unreliable" will drop a join that is correct.
Why it matters
catalog/gbif/README.mdsells"Partition pruning: h0 partitioning eliminates entire directories from query". A consumer
holding an
h8and pruning toh0 = h3_cell_to_parent(h8,0)misses ~8.7% of GBIF rows and~7.7% of OBIS rows — and 0% in every cng-datasets collection. That asymmetry is the
trap: a pattern validated on WDPA, IUCN or coral-reefs transfers to GBIF/OBIS and returns
quietly incomplete results, with no error raised.
h3:parent_resolutionsis[0], soh0ispurely a partition key with no intermediate resolution to absorb the discrepancy. GBIF at
least carries h1–h10.
GROUP BY h5thenaggregating to
h2viacell_to_parentdoes not match a directGROUP BY h2.Options
warn against deriving the partition key via
cell_to_parent, and say explicitly thath8joins are unaffected. Does not fix rollup.
cell_to_parentfrom a singlenative cell, matching the rest of the catalog. Makes all resolutions mutually consistent and
pruning-safe, at the cost of each
h_kno longer being the exact cell containing the point atresolution k. That is a real semantic trade, not a pure fix — decide it deliberately.
public-obishas an open re-hex to native h10 (Re-hex OBIS to native h10 (built at h8), from the staged 2026-09-09 raw #664) and an openingest PR (OBIS native ingest: 228.6M marine occurrence records at H3 resolution 8 (#660) #661). That re-hex rebuilds these columns regardless, so if the fix is
cell_to_parent, it costs nothing extra there.verify-stac.pycould assert the parent-chain property so a future bespokepipeline does not reintroduce it silently.
Provenance
Surfaced from #695 (which needs only that its grouping key be a prefix of the sort key, and is
unaffected by this). Raised by a parallel audit session; the survey above was contributed there
and every figure was re-measured independently here via the duckdb-geo MCP.