Skip to content

Audit + rebuild vector hex assets built before 2026-07-12: duplicate (feature, cell) rows from multipart polyfill #509

Description

@cboettig

Follow-up to the audit #387 asked for and #390 never did catalog-wide ("Worth auditing whether other hex assets built by the same step have the same defect"). Surfaced again while building #506.

The defect

The vector H3 polyfill emitted one row per (feature, cell, geometry part) instead of per (feature, cell), so any MultiPolygon whose parts share a cell produced byte-identical duplicate rows. COUNT(*), SUM(...), and SUM(h3_cell_area(...)) over such a hex asset all over-count.

This is already fixed upstreamboettiger-lab/datasets#150, PR #158, fix(#150): de-duplicate (feature, cell) rows in vector hex output, merged 2026-07-12 (Pass 2 now writes SELECT DISTINCT *). There is no tool bug to file and nothing to change in cng-datasets. The remaining problem is purely stale artifacts: every vector hex built before 2026-07-12 still carries the duplication.

Verified, not inferred

Ran the tool at :latest on the cluster with the exact production parameters of the conserved-areas hex job (--chunk-size 1000 --intermediate-chunk-size 10 --resolution 10 --parent-resolutions 9,8,0), chunk 4 (1,000 features):

rows duplicate (feature, cell) rows
Tool :latest, same params, same input 26,752 0
Published data for those same 1,000 features 26,889 137

Minimal reproducer of the mechanism — _cng_fid = 4661, "City Hall Park", a 2-part MultiPolygon of 9 total vertices covering 0.7 acres, both parts inside one res-10 cell. Pass 1 emits 2 rows ("Processing 2 rows in 2 batches"); the post-fix Pass 2 collapses them to 1. The published asset has 2.

Confirmed affected

Collection Hex built rows duplicate (feature, cell) rows
ca30x30-conserved-areas-terrestrial-2025 2026-06-18 13,340,094 70,806 (0.53%)
padus-4-1/fee 2026-02-15 609,968,495 160,293 (0.026%)
ca30x30-ecoregion rebuilt 2026-08-05 25,596,931 0 — fixed by the #507 res-10 rebuild on the post-fix tool

Worst single case in conserved-areas: 221 copies of one unit on a single res-10 cell (Black Mountain Ranch Open Space, 1,492 parts → 86 copies on one cell). Duplication scales with part count, so parcel/BLM-style multipart inventories are the exposed class; single-part polygons are unaffected.

Why it matters beyond a row count

_cng_fid is documented as the universal per-feature dedup key, and the hex assets STAC states "one row per (feature, h10) pair". Neither holds on a pre-fix build, so the guidance in AGENTS.md ("dedup by _cng_fid") does **not** rescue a consumer here — COUNT(DISTINCT _cng_fid)is fine, butCOUNT(*)and any per-cellSUMare inflated, andaudit-feature-dup.py --key _cng_fid` reports the asset as clean because it audits upstream (axis-2) duplication, not build artifacts.

The #506 hex-weights build dedups (cell, unit) explicitly before aggregating, so the published per-cell GAP fractions are correct and this issue does not block them. But it silently corrupted the issue`s own premise: the reported "max total weight 222" was 221 duplicate rows, not 222 overlapping units (real max: 16).

Proposed scope

  1. Enumerate. For every vector hex asset in the catalog, read the h0=*/data_0.parquet mtime (rclone metadata listing, not a scan). Anything ≥ 2026-07-12 is clean by construction; only earlier builds are suspect.
  2. Measure each suspect: COUNT(*) - COUNT(DISTINCT (h<native>, _cng_fid)) via the duckdb-geo MCP.
  3. Remediate each affected asset with a SELECT DISTINCT * rewrite to a staging prefix → validate row delta + unchanged COUNT(DISTINCT h<native>) → purge-verify-empty → flip. This is the fix(#387,#389): dedup ca30x30 ecoregion hex + exact h3_cell_area area recipe #390 ecoregion-dedup.yaml pattern; it is far cheaper than a full re-hex and is provably equivalent, because the duplicates are byte-identical. A full re-hex is only needed if an asset also predates another polyfill fix (e.g. the transmeridian split, datasets#145, 2026-07-10 — relevant to global layers).
  4. Verify rows == COUNT(DISTINCT (h<native>, _cng_fid)) afterwards, and consider adding that as a verify-stac.py data-backed check so a stale build cannot pass the gate silently.

REQUIRED: audit everything published before the gate existed

Scope amendment (2026-08-05). This issue is not complete when the known-affected assets are
fixed — it requires a systematic audit of all data predating the CI gate, because the two
defect classes found so far were both invisible for the same structural reason, not by chance:

So the pre-gate cohort has never been evaluated by the current rule set at all. Two confirmed
consequences already: the duplicate rows in this issue, and #511 (WDPA NO_TAKE = 'All ' with a
trailing space, which silently empties WHERE NO_TAKE = 'All').

Required work: run the current verify-stac.py (full, data-backed) against every collection
published before the gate — not just the row-uniqueness check — and triage every HARD finding.
Expect false positives from checks meeting old data for the first time: two were already found
(NULL-native hex columns fabricating 77,991,738 WDPA duplicates; a legitimate None category
discarded as a missing-value token, #511). Fix the checks as well as the data — a check that
misfires on the pre-gate cohort will misfire on new datasets too.

Acceptance for this half: every collection has been run against the current verifier at least
once, with each HARD finding either remediated or explicitly dismissed with a reason.

Acceptance: every vector hex asset in the catalog satisfies COUNT(*) == COUNT(DISTINCT (h<native>, _cng_fid)), except where a genuine multi-feature-per-cell case is documented on the asset.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions