Skip to content

Guard against unusable boundary geometry, and tooling to repair it - #86

Open
mikaalnaik wants to merge 1 commit into
mainfrom
mikaal/boundary-geometry-guard
Open

Guard against unusable boundary geometry, and tooling to repair it#86
mikaalnaik wants to merge 1 commit into
mainfrom
mikaal/boundary-geometry-guard

Conversation

@mikaalnaik

Copy link
Copy Markdown
Contributor

Follow-up to #82. Production has 73,390 boundaries whose geometry is unusable — polygons spanning the globe — and nothing noticed for months. #82 fixed the cause for CSD (StatCan files ship in EPSG:3347 and weren't being reprojected) and I reloaded CSD by hand after deploying. This adds the guard so it can't recur silently, plus the tooling to repair the remaining nine types.

The guard

A shapefile loaded without reprojecting from its source SRID yields coordinates in metres, which the geography column silently wraps into valid-looking degrees. Row counts, column types, and ingestion status all look healthy while every polygon is in the wrong place. That's why this went unnoticed.

The loader now checks the extent of what it just wrote against Canada, inside the transaction, so a bad load rolls back instead of replacing good geometry with garbage.

Verified by reverting the reprojection fix and loading the real CSD file:

raised: csd geometry falls outside Canada (lon -180.0..180.0, lat -90.0..90.0)
        — the shapefile was probably not reprojected from its source SRID. Nothing was written.
ingestion status: failed
csd rows before=5142 after=5142   (unchanged)
rows attributed to this ingestion: 0

The guard caught me out once, correctly. My first ceiling was 84°N — Canada's northernmost land is 83.1°N at Cape Columbia — and it rejected a correct FSA load reaching 89.999°N. StatCan draws Arctic boundaries to the pole (X0E in NWT, X0A in Nunavut), so the limit is now 90 and longitude does the discriminating work: wrapped metres spread across the full -180..180, which no Canadian file does. Confirmed the load was genuinely good by checking M5V's centroid, which lands on downtown Toronto.

Repair tooling

  • Fetcher#fetch(force:) — checksum dedupe answers "did upstream change", which is the wrong question when the loader changed and the rows it wrote are wrong. Replaces flipping ingestion status by hand (what I did in production).
  • geo:verify_boundaries — reports geometry health per boundary type and exits non-zero if any is outside Canada. This is the check that should have existed.
  • geo:reload_boundaries[source_name] — forces a re-download and reload; no argument does every boundary source.

Exercised end to end on the real FSA file: 1,641 boundaries, extent correct, M5V centroid on downtown Toronto.

Still broken in production, and not fixed here

Nine boundary types remain corrupt. They pre-date #82 and this PR only gives us the means to fix them:

Type Rows State
da 57,936 garbage
ct 6,245 garbage
fsa 1,641 garbage
popctr 1,030 garbage
ped 451 garbage
cd / cma / er / pr 538 garbage
csd 5,142 fixed (reloaded post-deploy)
fed 343 fine — always declared its projection

The crosswalks are affected too. geo:build_crosswalk assigns DAs to ridings by point-in-polygon on DA centroids, so every spatial crosswalk was computed from garbage geometry and needs rebuilding after the reload. Anything reading /api/v1/geo/boundaries, /api/v1/geo/crosswalk, or the crosswalk tables is currently getting wrong answers.

Reload is the DA file first (it's the big one and the crosswalks depend on it), then the rest, then geo:build_crosswalk. I'd rather run that deliberately with someone watching than fold it into this PR.

Testing

852 runs, 2801 assertions, 0 failures. RuboCop clean across 520 files. Guard coverage includes the wrapped-globe case, the Arctic-to-the-pole case, an empty load, and force reusing the ingestion row rather than duplicating it.

🤖 Generated with Claude Code

Production had 73,390 boundaries whose geometry was unusable — polygons
spanning the globe — and nothing had noticed, because a shapefile loaded
without reprojecting from its source SRID yields coordinates in metres that
the geography column silently wraps into valid-looking degrees. Row counts,
column types and the ingestion status all looked healthy.

The loader now checks the extent of what it just wrote against Canada, inside
the transaction, so a bad load rolls back rather than replacing good geometry
with garbage. Verified by reverting the reprojection fix and loading the real
CSD file: it raises with the offending bbox, marks the ingestion failed, and
writes nothing.

Longitude does the real work in that check. The northern limit is 90 rather
than Canada's northernmost land, because StatCan draws Arctic boundaries to
the pole — FSAs X0E and X0A both reach 89.999, and a tighter ceiling rejected
every correctly-projected file covering the north. I found that by having the
guard reject a load that was in fact correct.

Also adds the two things needed to repair the existing rows:

  * Fetcher#fetch(force:) — checksum dedupe answers "did upstream change",
    which is the wrong question when the loader changed and the rows it wrote
    are wrong. Replaces flipping ingestion status by hand.
  * geo:verify_boundaries to report geometry health per boundary type, and
    geo:reload_boundaries[source] to force a reload. Verified end to end on
    the real FSA file: 1,641 boundaries, M5V's centroid landing on downtown
    Toronto.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant