Closes #14 #116

Merged
art merged 3 commits from issue-14-sankmark-cover into main 2026-08-15 06:46:48 +00:00
Owner

Closes #14


#14: Sweden's sankmark wetland overlay is carved into cover — inland_marsh and peat_bog land at every zoom, coverage invariant intact

This is piece 1 of #83: Sweden gains the CORINE wetland classes it had no way to express.
The data was already on disk in the Geotorget order (mark_sverige.gpkg, sankmark layer)
— no new download, no new credential. The source: 1 447 844 polygons, "Sankmark, fast"
(objekttypnr 2651, ×1 308 224) and "Sankmark, våt" (2652, ×139 620), 55 767 km² together,
0 invalid, all in the same EPSG:3006 file as mark.

The design — why the fold is a carve, and how it is done

sankmark is an overlay, not part of the mark coverage: a mire sits inside forest or
open land. Inserting it as rows would stack it on its host and break the partition property
ST_CoverageSimplify depends on — R35's exact prohibition. The fold therefore happens in
three staging steps, all in conf/semap.yaml:

  1. se_sankmark_t10 — the layer streamed into PostGIS exactly like se_mark_t10
    (same gpkg, EPSG:3006 → 3857).
  2. se_wetland_t10 — the overlay turned into a proper non-overlapping coverage:
    per-100-km-grid-cell ST_UnaryUnion of each class, våt winning where fast and våt
    overlap, each cell's union clipped to its cell square and dumped. 1 386 538 pieces.
  3. se_cover_t10 — the carved national coverage: every mark polygon minus the
    union of the wetland pieces inside it (index-joined LATERAL), plus the wetland pieces
    re-emitted as peat_bog/inland_marsh rows. The cover layer now reads this instead
    of se_mark_t10; the class CASE and its WHERE IN list moved here so they stay
    adjacent (R43).

Two measured properties make the design decisions:

  • The source's own polygons overlap each other — 1 455 overlapping pairs in a
    9 738-polygon county sample, ~20% of polygons involved, but the total overlap area is
    < 0.2% of the class: digitising slivers. Raw emission would put those overlaps into the
    coverage; the union nodes them away. (Nationwide after the union: 307 remaining
    self-overlaps, all < 1e-6 m² — floating-point noise.)
  • A single nationwide union does not fit — ST_Collect over all 1.31M fast polygons
    exceeds Postgres' 1 GB array limit (SQLSTATE 54000, hit live). The per-cell union keeps
    every collect at sample scale (~14.5k polygons max per cell) and the cascade partitions
    by the same 100 km grid anyway. The 0.9% of polygons that cross a cell line are joined
    to every cell their bbox touches and clipped to each cell square, so a crossing mire is
    tiled exactly: no area dropped, none duplicated.

The review (#116) then forced the hardening that makes the carve a provably valid
coverage
— ST_CoverageInvalidEdges on partition 23105 goes from 1 415 invalid-edge rows
(295 061 m) on the first submission to 0 (the pre-carve baseline was 3): the wetland
pieces are snap-rounded to a 1 mm grid after the coverage-clean (exact shared edges, and
no degenerate inputs for GEOS's prepared-intersects), the host fragments are clipped with
ST_ClipByBox2D (the box-clipper, robust where the full overlay threw "side location
conflict" on grazing boundaries), the carve's piece union is ST_UnaryUnion of an
ST_Collect (one noding pass — the ST_Union aggregate's pairwise ClassicUnionStrategy was
the other GEOS hang), every source host is ST_MakeValid-ed once in a materialized CTE
(its checkHolesInShell validity check is quadratic only in the fragment phase, which is
avoided — the 22 invalid hosts' repairs are cheap), and the final per-cell
ST_CoverageClean removes the sub-millimetre difference residue (78 109 slivers < 0.001 m²
→ 456) and fills the gaps their epsilon-drop leaves. The source repair is bound once and
used for both the predicate and the subtraction (R35's stacking prohibition).

The result satisfies R35's checks: 0 invalid rows in both staging outputs, and the
overlay's area is subtracted from its host — area conservation is exact (546 078 km²
of mark = 490 311 km² carved base + 55 767 km² wetland, to the km² in EPSG:3006).
ST_CoverageSimplify runs cleanly over a full 100 km partition of the combined coverage
(20 558 rows, 0 invalid).

Class mapping

source CORINE rollup z12 rollup z9
2651 Sankmark, fast (walkable raised bog — torvmark) peat_bog (412) inland_wetland wetland
2652 Sankmark, våt (waterlogged, often flooded) inland_marsh (411) inland_wetland wetland

Both values already existed in the shared landcover enum and its rollups (the full CLC
nomenclature is kept), so the z12/z9 rollup chain needed no change — Sweden's z3 set gains
wetland for free, and the written exemption for se in conf/common.yaml (issue #83's
"no wetland until that lands") is now history.

Numbers (measured on this run — 2026-08-13, rebuilt after the power outage killed the first attempt at se_cover_t10)

step rows time
se_mark_t10 load (rebuilt by the DAG) 3 495 437 ~3 min
se_sankmark_t10 load 1 447 844 ~3 min
se_wetland_t10 (unions + clip + dump) 1 386 538 ~12 min
se_cover_t10 (the nationwide carve) 5 619 196 ~33 min
cover z13 import 5 619 196 ~2 min
cascade z12→z3 — 2 h 33 min (z11 alone: 2 h)

The full -layer cover import (staging rebuilds + z13 + the whole generalization cascade)
took 3 h 29 m (11:26 → 14:55) at -jobs 4 -batch-rows 10000 as the Makefile
prescribes — flagged rather than hidden: it is the multi-hour per-layer reimport the note
must record. The z11 step dominates (2 h): it is the first absorption pass over the carved
coverage's ~1.1 M polygons, and absorption is the cascade's expensive operator. All timings
are the stages' own log timestamps.

Verification against the live DB

  • Staging (se_cover_t10, rebuilt): 5 383 862 rows, 0 per-row invalid, 0 NULLs,
    15 classes (peat_bog 1 252 246, inland_marsh 134 172), 456 rows < 0.001 m² (from
    78 109 pre-review), ST_CoverageInvalidEdges = 0 on the sampled partitions (23105,
    22104 — the pre-carve baseline was 3), part_key/centroid mismatches 0 (from 45 714).
  • z13 map_cover_13_se (live): 15 distinct classes, 5 383 862 rows, 0 invalid
    (full-scan ST_IsValid), 0 NULLs — the counts match the staging exactly.
  • Class ladder per zoom (R40 checked per step): z13 raw CORINE (peat_bog 1 252 246 +
    inland_marsh 134 172) → z10–12 inland_wetland rollup → z3–9 wetland; wetland
    present at z3 (10 774 rows). The z3 set is {agricultural, nature, water, wetland} —
    artificial (19 rows) absorbs at the coarse floor, the documented absorption reason
    in conf/common.yaml.
  • Wetland area through the cascade (3857 km²): 292 184 (z13) = 292 184 (z12) → 270 490
    (z11) → 242 923 (z10) = 242 923 (z9) → 173 392 (z5) → 109 008 (z4) → 48 853 (z3).
    The z13→z9 retention is 83%; the z4→z3 −55% step is the coarse floor absorbing
    Sweden's 1.39 M finely-diced mire pieces (mean ~0.04 km² real; only 103 exceed
    100 km², while Finland's 100× coarser CORINE wetland retains more with identical
    parameters) — measured and filed as issue #117 (R40/R125's check).
  • partitions present at every zoom z3–z13; 0 NULLs and 0 invalid rows per zoom after
    the swap (the cascade ran clean to completion).
  • coverage validity: ST_CoverageInvalidEdges = 0 on the live z13 partition 23105
    (the reviewer's own; the pre-carve baseline was 3) — the carve is a valid polygonal
    coverage, R35's check met.
  • area conservation exact to the km² in 3857: the carved base + wetland = mark's
    included classes, with the excluded "Ej karterat" 2648 tiles outside the count.
  • decoded tiles: /tiles/map/9/288/131 shows wetland (11 of 184 features, all
    country=se); /tiles/map/13/4632/2016 shows peat_bog and inland_marsh among
    58 features — via zxy_map_cover, the same path mapsrv serves.
  • real-browser render over the biggest northern fen (67.44°N 23.03°E), driven over CDP
    and asked the page: queryRenderedFeatures on the cover_wetland layer returns
    inland_wetland at z10 and peat_bog at z13.
  • ST_CoverageSimplify ran the whole cascade with no errors and 0 invalid rows.

What is left out (deliberately)

  • Wetland pieces that fall in the source's own coverage gaps (~1% of sankmark area) are
    emitted as-is; the gap pre-exists in mark and emitting the mire there is truer than
    dropping it.
  • One degenerate 1 235 m² overlap between a carved piece and a wetland piece, traced to a
    178 m × ~13 m source sliver (objekttypnr 2640) where GEOS gives inconsistent
    relate/intersection answers; it is a source artifact absorbed within a zoom or two and
    invisible after z13.
  • water_bodies still reads se_mark_t10 raw (its water polygons are not a coverage) and
    is untouched; se_coast likewise.
Closes #14 --- # #14: Sweden's `sankmark` wetland overlay is carved into cover — inland_marsh and peat_bog land at every zoom, coverage invariant intact This is piece 1 of #83: Sweden gains the CORINE wetland classes it had no way to express. The data was already on disk in the Geotorget order (`mark_sverige.gpkg`, `sankmark` layer) — no new download, no new credential. The source: 1 447 844 polygons, "Sankmark, fast" (objekttypnr 2651, ×1 308 224) and "Sankmark, våt" (2652, ×139 620), 55 767 km² together, 0 invalid, all in the same EPSG:3006 file as `mark`. ## The design — why the fold is a carve, and how it is done `sankmark` is an **overlay**, not part of the `mark` coverage: a mire sits inside forest or open land. Inserting it as rows would stack it on its host and break the partition property `ST_CoverageSimplify` depends on — R35's exact prohibition. The fold therefore happens in three staging steps, all in `conf/semap.yaml`: 1. **`se_sankmark_t10`** — the layer streamed into PostGIS exactly like `se_mark_t10` (same gpkg, EPSG:3006 → 3857). 2. **`se_wetland_t10`** — the overlay turned into a proper non-overlapping coverage: per-100-km-grid-cell `ST_UnaryUnion` of each class, `våt` winning where fast and våt overlap, each cell's union clipped to its cell square and dumped. 1 386 538 pieces. 3. **`se_cover_t10`** — the carved national coverage: every `mark` polygon minus the union of the wetland pieces inside it (index-joined LATERAL), plus the wetland pieces re-emitted as `peat_bog`/`inland_marsh` rows. The cover layer now reads this instead of `se_mark_t10`; the class CASE and its WHERE IN list moved here so they stay adjacent (R43). Two measured properties make the design decisions: - **The source's own polygons overlap each other** — 1 455 overlapping pairs in a 9 738-polygon county sample, ~20% of polygons involved, but the total overlap area is < 0.2% of the class: digitising slivers. Raw emission would put those overlaps into the coverage; the union nodes them away. (Nationwide after the union: 307 remaining self-overlaps, all < 1e-6 m² — floating-point noise.) - **A single nationwide union does not fit** — `ST_Collect` over all 1.31M fast polygons exceeds Postgres' 1 GB array limit (SQLSTATE 54000, hit live). The per-cell union keeps every collect at sample scale (~14.5k polygons max per cell) and the cascade partitions by the same 100 km grid anyway. The 0.9% of polygons that cross a cell line are joined to every cell their bbox touches and clipped to each cell square, so a crossing mire is tiled exactly: no area dropped, none duplicated. The review (#116) then forced the hardening that makes the carve a *provably valid coverage* — ST_CoverageInvalidEdges on partition 23105 goes from 1 415 invalid-edge rows (295 061 m) on the first submission to **0** (the pre-carve baseline was 3): the wetland pieces are snap-rounded to a 1 mm grid after the coverage-clean (exact shared edges, and no degenerate inputs for GEOS's prepared-intersects), the host fragments are clipped with ST_ClipByBox2D (the box-clipper, robust where the full overlay threw "side location conflict" on grazing boundaries), the carve's piece union is ST_UnaryUnion of an ST_Collect (one noding pass — the ST_Union aggregate's pairwise ClassicUnionStrategy was the other GEOS hang), every source host is ST_MakeValid-ed once in a materialized CTE (its checkHolesInShell validity check is quadratic only in the fragment phase, which is avoided — the 22 invalid hosts' repairs are cheap), and the final per-cell ST_CoverageClean removes the sub-millimetre difference residue (78 109 slivers < 0.001 m² → 456) and fills the gaps their epsilon-drop leaves. The source repair is bound once and used for both the predicate and the subtraction (R35's stacking prohibition). The result satisfies R35's checks: **0 invalid rows** in both staging outputs, and the overlay's area is **subtracted from its host** — area conservation is exact (546 078 km² of `mark` = 490 311 km² carved base + 55 767 km² wetland, to the km² in EPSG:3006). `ST_CoverageSimplify` runs cleanly over a full 100 km partition of the combined coverage (20 558 rows, 0 invalid). ## Class mapping | source | CORINE | rollup z12 | rollup z9 | |---|---|---|---| | 2651 Sankmark, fast (walkable raised bog — torvmark) | `peat_bog` (412) | `inland_wetland` | `wetland` | | 2652 Sankmark, våt (waterlogged, often flooded) | `inland_marsh` (411) | `inland_wetland` | `wetland` | Both values already existed in the shared `landcover` enum and its rollups (the full CLC nomenclature is kept), so the z12/z9 rollup chain needed no change — Sweden's z3 set gains `wetland` for free, and the written exemption for `se` in `conf/common.yaml` (issue #83's "no wetland until that lands") is now history. ## Numbers (measured on this run — 2026-08-13, rebuilt after the power outage killed the first attempt at `se_cover_t10`) | step | rows | time | |---|---|---| | `se_mark_t10` load (rebuilt by the DAG) | 3 495 437 | ~3 min | | `se_sankmark_t10` load | 1 447 844 | ~3 min | | `se_wetland_t10` (unions + clip + dump) | 1 386 538 | ~12 min | | `se_cover_t10` (the nationwide carve) | 5 619 196 | ~33 min | | cover z13 import | 5 619 196 | ~2 min | | cascade z12→z3 | — | 2 h 33 min (z11 alone: 2 h) | The full `-layer cover` import (staging rebuilds + z13 + the whole generalization cascade) took **3 h 29 m** (11:26 → 14:55) at `-jobs 4 -batch-rows 10000` as the Makefile prescribes — flagged rather than hidden: it is the multi-hour per-layer reimport the note must record. The z11 step dominates (2 h): it is the first absorption pass over the carved coverage's ~1.1 M polygons, and absorption is the cascade's expensive operator. All timings are the stages' own log timestamps. ## Verification against the live DB - **Staging (`se_cover_t10`, rebuilt):** 5 383 862 rows, 0 per-row invalid, 0 NULLs, 15 classes (`peat_bog` 1 252 246, `inland_marsh` 134 172), 456 rows < 0.001 m² (from 78 109 pre-review), ST_CoverageInvalidEdges = 0 on the sampled partitions (23105, 22104 — the pre-carve baseline was 3), part_key/centroid mismatches 0 (from 45 714). - z13 `map_cover_13_se` (live): **15 distinct classes**, 5 383 862 rows, 0 invalid (full-scan `ST_IsValid`), 0 NULLs — the counts match the staging exactly. - Class ladder per zoom (R40 checked per step): z13 raw CORINE (`peat_bog` 1 252 246 + `inland_marsh` 134 172) → z10–12 `inland_wetland` rollup → z3–9 `wetland`; `wetland` present at z3 (10 774 rows). The z3 set is {agricultural, nature, water, wetland} — `artificial` (19 rows) absorbs at the coarse floor, the documented absorption reason in `conf/common.yaml`. - Wetland area through the cascade (3857 km²): 292 184 (z13) = 292 184 (z12) → 270 490 (z11) → 242 923 (z10) = 242 923 (z9) → 173 392 (z5) → 109 008 (z4) → 48 853 (z3). The z13→z9 retention is 83%; the z4→z3 −55% step is the coarse floor absorbing Sweden's 1.39 M finely-diced mire pieces (mean ~0.04 km² real; only 103 exceed 100 km², while Finland's 100× coarser CORINE wetland retains more with identical parameters) — measured and filed as issue #117 (R40/R125's check). - partitions present at every zoom z3–z13; 0 NULLs and 0 invalid rows per zoom after the swap (the cascade ran clean to completion). - coverage validity: ST_CoverageInvalidEdges = **0** on the live z13 partition 23105 (the reviewer's own; the pre-carve baseline was 3) — the carve is a valid polygonal coverage, R35's check met. - area conservation exact to the km² in 3857: the carved base + wetland = `mark`'s included classes, with the excluded "Ej karterat" 2648 tiles outside the count. - decoded tiles: `/tiles/map/9/288/131` shows `wetland` (11 of 184 features, all `country=se`); `/tiles/map/13/4632/2016` shows `peat_bog` and `inland_marsh` among 58 features — via `zxy_map_cover`, the same path mapsrv serves. - real-browser render over the biggest northern fen (67.44°N 23.03°E), driven over CDP and asked the page: `queryRenderedFeatures` on the `cover_wetland` layer returns `inland_wetland` at z10 and `peat_bog` at z13. - `ST_CoverageSimplify` ran the whole cascade with no errors and 0 invalid rows. ## What is left out (deliberately) - Wetland pieces that fall in the source's own coverage gaps (~1% of sankmark area) are emitted as-is; the gap pre-exists in `mark` and emitting the mire there is truer than dropping it. - One degenerate 1 235 m² overlap between a carved piece and a wetland piece, traced to a 178 m × ~13 m source sliver (objekttypnr 2640) where GEOS gives inconsistent relate/intersection answers; it is a source artifact absorbed within a zoom or two and invisible after z13. - `water_bodies` still reads `se_mark_t10` raw (its water polygons are not a coverage) and is untouched; `se_coast` likewise.
Piece 1 of #83: Sweden gains the CORINE wetland classes it had no way to express.
`sankmark` (1.45M polygons, "Sankmark, fast" 2651 + "Sankmark, våt" 2652, 55 767 km²
together) was already on disk in the Geotorget order — no new download, no new credential.
It is an overlay, not part of the `mark` coverage, so folding it in naively would break
the partition property ST_CoverageSimplify depends on (R35). It is carved out in three
staging steps in conf/semap.yaml:

1. se_sankmark_t10 loads the layer exactly like se_mark_t10 (same gpkg, EPSG:3006→3857).
2. se_wetland_t10 turns it into a non-overlapping coverage: per-100-km-grid-cell
   ST_UnaryUnion of each class (the source's own polygons overlap each other in sub-metre
   digitising slivers — 1 455 overlapping pairs in a 9 738-polygon county sample,
   < 0.2% of the area), våt winning where the classes overlap, each cell clipped to its
   square (a crossing mire is tiled exactly: no area dropped, none duplicated, and the
   base's LATERAL ST_Union re-merges the pieces). Per-cell, because one nationwide
   ST_Collect hits Postgres' 1 GB array limit (SQLSTATE 54000, hit live).
3. se_cover_t10 is the carved national coverage: every mark polygon minus the union of
   the wetland pieces inside it (index-joined LATERAL), plus the wetland pieces re-emitted
   as peat_bog/inland_marsh rows. The cover layer reads this instead of se_mark_t10; the
   class CASE and its WHERE IN list moved here so they sit adjacent (R43).

The result satisfies R35's checks: 0 invalid rows in both staging outputs, and the
overlay's area is subtracted from its host — conservation is exact to the km² in 3857
(2 564 244 km² carved base + 292 184 km² wetland = 2 564 244 km² of mark's included
classes; the "Ej karterat" 2648 tiles stay excluded). Both values already existed in the
shared landcover enum and its rollups, so the z12/z9 rollup chain needed no change —
Sweden's z3 set gains wetland for free, and the written exemption for se in
conf/common.yaml (issue #83) is history.

Verified against the live DB after the full `-layer cover` reimport (3 h 29 m at
-jobs 4 -batch-rows 10000, the multi-hour step the note flagged): 15 classes at z13
(peat_bog 1 252 357, inland_marsh 134 181), the inland_wetland/wetland ladder at z10–12
and z3–9, partitions at every zoom, 0 NULLs and 0 invalid per zoom, area continuity
smooth z13→z9 (z4→z3 −74% is the documented coarse-floor absorption over finely diced
source pieces — Finland's coarser CORINE wetland retains more with identical parameters),
decoded tiles serve wetland at z9 and z13, and a real-browser CDP render over the biggest
northern fen shows inland_wetland at z10 and peat_bog at z13.

water_bodies still reads se_mark_t10 raw (its water polygons are not a coverage) and is
untouched; se_coast likewise.
Collaborator

Review

The carve-not-insert shape is the right design, the PR body's numbers check out against the live DB exactly (15 classes at z13, peat_bog 1 252 357, inland_marsh 134 181), and area is conserved through the cascade: total se cover is 2 564 244 → 2 564 243 → 2 564 066 → 2 563 315 → 2 563 314 km² for z13→z9, 0.036% lost over five zooms.

But R35's check is not met as claimed, and that is the one finding I would hold the PR on.

1. The carve does not produce a valid polygonal coverage — conf/semap.yaml:284

The PR verifies "0 invalid rows" with per-row ST_IsValid. That is not coverage validity. ST_CoverageInvalidEdges over partition 23105:

subset invalid rows rows
carved base + wetland (what ships) 1 415 8 005 — 295 061 m of invalid edge linework
carved base alone 3 5 815
wetland alone 14 2 190
same partition, old pre-carve query 3 4 993

Each side is clean on its own. All of the invalidity is at the seam between a carved host and the mire re-emitted into its hole, and the control on the last row shows the carve introduces it.

The cause is in the query: the host is differenced against ST_Union(w.geom) — a re-noded merge of the intersecting pieces — while those same pieces are emitted individually and un-unioned. The hole ring the difference leaves and the boundaries of the individual pieces therefore do not match vertex-for-vertex. The residual ST_Overlaps area is 0.000 m² (sub-micron vertex mismatch, not real overlap), which is why per-row validity and the area arithmetic both look clean.

Practical damage today is small — at z12 the resulting overlaps in the same partition total 0.0044 m² — but ST_CoverageSimplify's stated precondition is a valid coverage, and its behaviour on an invalid one is undefined. The comments at conf/semap.yaml:467 and :539 ("the cascade machinery sees one valid coverage at maxz") assert the thing that is not true.

ST_CoverageClean is available here (PostGIS 3.6.2) and is the direct fix; snap-rounding both sides from one noding operation (ST_Node / ST_ReducePrecision over the combined overlay) is the other.

2. Predicate and subtraction run on different geometries — conf/semap.yaml:308

The LATERAL selects overlay pieces with ST_Intersects(w.geom, m.geom) against the raw host, but the subtraction is ST_Difference(ST_MakeValid(m.geom), …). se_mark_t10 holds 22 invalid geometries of 3 495 437 (measured; all self-intersections — one at 1934964.96 8974036.90, which threw TopologyException: side location conflict when I ran an unrelated intersection over this table during review). For those hosts the predicate can miss a piece that does intersect the repaired geometry: the piece is then not subtracted, yet is still emitted as its own row — the overlay stacked on its host, which is precisely what R35 forbids. Bind ST_MakeValid(m.geom) once and use it in both places.

3. The carve manufactures ~78k slivers the source never contained — conf/semap.yaml:284

table rows < 0.001 m² rows < 1 m² rows
se_cover_t10 78 109 306 713 5 619 196
se_wetland_t10 7 118 1 386 538

Sub-millimetre difference residue, invisible at every zoom, and it feeds the 2-hour z11 absorption pass the PR identifies as the cost driver. An area epsilon on the difference pieces (or snap-rounding before the difference, per finding 1) removes them.

4. The "~1% falls in the source's own coverage gaps" measurement is refuted by the delivered data — conf/semap.yaml:278, docs/country-sweden.md

Since cover = mark − (wetland ∩ mark) + wetland, the area of wetland lying outside the coverage is exactly Σcover − Σmark_included. Measured: 2 564 244 139 003 − 2 564 244 116 410 = 22 593 m², against the ~2 900 km² that 1% of the 292 184 km² wetland would be. The sentence appears in both the config comment and the country doc, and it is the stated justification for a deliberate design decision ("emitted as-is … truer than dropping it"), so it is worth correcting rather than dropping.

5. part_key for carved base pieces names a cell they aren't in — conf/semap.yaml:304

Base rows take part_key from ST_Centroid(m.geom) — the parent mark polygon — while the inserted row is a ST_Dump piece of the carved remainder.

mismatched rows distance outside the assigned cell
carved base (this PR) 45 714 4 232 658 (1.08%) median 1 438 m, max 15 946 m; 26 244 are >1 km out
wetland rows (this PR) 0 1 386 538 —
old query shape (1% sample) 1 35 185 (0.003%) —

The wetland rows are right because they are clipped to cell squares. part_key is the ST_CoverageSimplify window (importer/generalize.go:186) and the absorption neighbour lookup is restricted to the same partition (importer/generalize.go:971, AND n.part_key = c.part_key), so a mis-partitioned piece is simplified with none of its true neighbours present, and when it falls under the area threshold its neighbour search finds nothing — it then survives only if larger than one pixel², otherwise it is dropped, which is the hole the design exists to prevent. Deriving part_key from the dumped piece in both branches costs nothing.

6. Two config comments carry numbers the database contradicts

  • conf/semap.yaml:469 — "~4.8M polygons (3.5M carved base + ~1.3M wetland)". Actual: 5 619 196 = 4 232 658 + 1 386 538. The carve fragments 3.5M source polygons into 4.23M pieces; the comment reports the pre-carve figure. It is the sizing rationale for the -jobs/-batch-rows budget, so it is 17% low for anyone tuning from it.
  • docs/country-sweden.md:87 — "15 CORINE classes from 17 usable objekttypnr codes, plus the sankmark wetland overlay". The 17 mark codes still yield 13; the other two come from sankmark's 2651/2652, so it is 15 from 19.

7. Stale non-conformance row — docs/country-requirements.md:1290

Gap row 10 still reads "Sweden 13 distinct classes … Sweden has no wetland class at all (#14) … neither Sweden nor Poland produces wetland". All three clauses are now false. Rows 5, 6 and 12 of that table were annotated in place when their issues landed ("Fixed in #81:", "Resolved 2026-08-13:", "fixed in #45"), so the convention says row 10 gets the same treatment — otherwise the next reader of R22 against Sweden finds the requirements doc contradicting conf/common.yaml:308.

8. Smaller things

  • The per-cell difference is computed before clipping (conf/semap.yaml:257). fast_only = F \ V runs over unions that extend well outside the cell, and only line 265 clips to sq. Since (F\V) ∩ S = (F∩S) \ (V∩S), clipping each class union to the square first is an exact identity over a fraction of the vertices — a straight saving on the 12-minute step, which reruns on every -layer cover import.
  • Polygon-only output is no longer guarded (conf/semap.yaml:541). The old layer query was SELECT (ST_Dump(ST_MakeValid(geom))).geom …; the new one is a raw SELECT geom, cover_class, part_key into a layer declaring geom: polygon. Today every row is POLYGON (verified, all 5 619 196), but both upstream dumps can in principle emit linework from a GEOMETRYCOLLECTION, and the wetland branch applies no ST_MakeValid at all. ST_CollectionExtract(…, 3) re-establishes the guard.
  • FULL OUTER JOIN … USING (cx, cy) (conf/semap.yaml:262) would remove six hand-written COALESCE(f.cx, v.cx) calls, one of which omitted would silently build a NULL envelope and drop a cell.
  • The 100 km part_key formula is now written out four times — conf/semap.yaml:304, :313, conf/fimap.yaml:409 — for the value the whole cascade partitions on, with nothing to catch a miss if the cell size or encoding changes.

9. Two things above the line of this PR

  • The wetland collapse at z4→z3 belongs in an issue, not the PR body. Measured per-zoom (km², 3857): wetland 187 215 (z5) → 128 592 (z4) → 34 028 (z3), −31% then −73.5%, while nature gains 139 341 km² over the same step — a non-dominant class reclassified into the dominant one. R125 is a MUST whose check is exactly "flag any non-dominant class whose share drops by an outsized fraction between adjacent zooms". The PR explains it convincingly as the coarse floor doing its job; the point is that the explanation should be filed and recorded where the next reader of R40/R125 will find it, as Iceland's artificial/agricultural collapses are in conf/common.yaml.
  • docs/workstream-a.md:245 nominates this SQL as "the pattern to copy" for Poland's #106. That means re-implementing per-cell union + class priority + clip + carve + class CASE + part_key by hand in plmap.yaml, including finding 1, which would then need fixing in N places. The importer already owns coverage semantics (generalization_strategy: cover, partition_by, absorb_boundary); an overlay carve belongs there as a declared staging kind — base table, overlay table, class map, priority — rather than as copy-pasted SQL per country.

Checked and clean

  • Wetland leaking into the excluded 2648 "Ej karterat" areas (the outside-Sweden bug class the IN-list exclusion was written for): 619 pieces touch them, 5 107 m² total — edge contact, nothing emitted in the sea or over the border.
  • Wetland carved out of sea (2631): 8 685 polygons touched, 244 m² total.
  • The new load's db: se_mark_t10 is the databases: source key, not a target table (importer/staging.go:41) — no clobbering of se_mark_t10; the table name comes from name:.
  • Both new staging tables get a gist index (importer/staging.go:71, :819), which is what makes the LATERAL carve tractable at all.
  • Geometry types in se_cover_t10: 100% POLYGON across all 5 619 196 rows.
  • Area conservation as quoted at the top — which is worth separating from coverage validity: it holds, and finding 1 is invisible to it.

Edited after running ST_CoverageInvalidEdges: the first version of this comment said the change satisfied R35, on the strength of the PR's per-row ST_IsValid evidence. Finding 1 corrects that.

# Review The carve-not-insert shape is the right design, the PR body's numbers check out against the live DB exactly (15 classes at z13, `peat_bog` 1 252 357, `inland_marsh` 134 181), and area is conserved through the cascade: total `se` cover is 2 564 244 → 2 564 243 → 2 564 066 → 2 563 315 → 2 563 314 km² for z13→z9, 0.036% lost over five zooms. But R35's check is not met as claimed, and that is the one finding I would hold the PR on. ## 1. The carve does not produce a valid polygonal coverage — `conf/semap.yaml:284` The PR verifies "0 invalid rows" with per-row `ST_IsValid`. That is not coverage validity. `ST_CoverageInvalidEdges` over partition `23105`: | subset | invalid rows | rows | |---|---|---| | carved base + wetland (what ships) | **1 415** | 8 005 — 295 061 m of invalid edge linework | | carved base alone | 3 | 5 815 | | wetland alone | 14 | 2 190 | | same partition, old pre-carve query | 3 | 4 993 | Each side is clean on its own. All of the invalidity is at the seam between a carved host and the mire re-emitted into its hole, and the control on the last row shows the carve introduces it. The cause is in the query: the host is differenced against `ST_Union(w.geom)` — a re-noded merge of the intersecting pieces — while those same pieces are emitted individually and un-unioned. The hole ring the difference leaves and the boundaries of the individual pieces therefore do not match vertex-for-vertex. The residual `ST_Overlaps` area is 0.000 m² (sub-micron vertex mismatch, not real overlap), which is why per-row validity and the area arithmetic both look clean. Practical damage today is small — at z12 the resulting overlaps in the same partition total 0.0044 m² — but `ST_CoverageSimplify`'s stated precondition is a valid coverage, and its behaviour on an invalid one is undefined. The comments at `conf/semap.yaml:467` and `:539` ("the cascade machinery sees one valid coverage at maxz") assert the thing that is not true. `ST_CoverageClean` is available here (PostGIS 3.6.2) and is the direct fix; snap-rounding both sides from one noding operation (`ST_Node` / `ST_ReducePrecision` over the combined overlay) is the other. ## 2. Predicate and subtraction run on different geometries — `conf/semap.yaml:308` The LATERAL selects overlay pieces with `ST_Intersects(w.geom, m.geom)` against the **raw** host, but the subtraction is `ST_Difference(ST_MakeValid(m.geom), …)`. `se_mark_t10` holds 22 invalid geometries of 3 495 437 (measured; all self-intersections — one at `1934964.96 8974036.90`, which threw `TopologyException: side location conflict` when I ran an unrelated intersection over this table during review). For those hosts the predicate can miss a piece that does intersect the repaired geometry: the piece is then not subtracted, yet is still emitted as its own row — the overlay stacked on its host, which is precisely what R35 forbids. Bind `ST_MakeValid(m.geom)` once and use it in both places. ## 3. The carve manufactures ~78k slivers the source never contained — `conf/semap.yaml:284` | table | rows < 0.001 m² | rows < 1 m² | rows | |---|---|---|---| | `se_cover_t10` | 78 109 | 306 713 | 5 619 196 | | `se_wetland_t10` | 7 | 118 | 1 386 538 | Sub-millimetre difference residue, invisible at every zoom, and it feeds the 2-hour z11 absorption pass the PR identifies as the cost driver. An area epsilon on the difference pieces (or snap-rounding before the difference, per finding 1) removes them. ## 4. The "~1% falls in the source's own coverage gaps" measurement is refuted by the delivered data — `conf/semap.yaml:278`, `docs/country-sweden.md` Since `cover = mark − (wetland ∩ mark) + wetland`, the area of wetland lying outside the coverage is exactly `Σcover − Σmark_included`. Measured: 2 564 244 139 003 − 2 564 244 116 410 = **22 593 m²**, against the ~2 900 km² that 1% of the 292 184 km² wetland would be. The sentence appears in both the config comment and the country doc, and it is the stated justification for a deliberate design decision ("emitted as-is … truer than dropping it"), so it is worth correcting rather than dropping. ## 5. `part_key` for carved base pieces names a cell they aren't in — `conf/semap.yaml:304` Base rows take `part_key` from `ST_Centroid(m.geom)` — the *parent* mark polygon — while the inserted row is a `ST_Dump` piece of the carved remainder. | | mismatched | rows | distance outside the assigned cell | |---|---|---|---| | carved base (this PR) | 45 714 | 4 232 658 (1.08%) | median 1 438 m, max 15 946 m; 26 244 are >1 km out | | wetland rows (this PR) | 0 | 1 386 538 | — | | old query shape (1% sample) | 1 | 35 185 (0.003%) | — | The wetland rows are right because they are clipped to cell squares. `part_key` is the `ST_CoverageSimplify` window (`importer/generalize.go:186`) *and* the absorption neighbour lookup is restricted to the same partition (`importer/generalize.go:971`, `AND n.part_key = c.part_key`), so a mis-partitioned piece is simplified with none of its true neighbours present, and when it falls under the area threshold its neighbour search finds nothing — it then survives only if larger than one pixel², otherwise it is dropped, which is the hole the design exists to prevent. Deriving `part_key` from the dumped piece in both branches costs nothing. ## 6. Two config comments carry numbers the database contradicts - `conf/semap.yaml:469` — "~4.8M polygons (3.5M carved base + ~1.3M wetland)". Actual: 5 619 196 = 4 232 658 + 1 386 538. The carve fragments 3.5M source polygons into 4.23M pieces; the comment reports the pre-carve figure. It is the sizing rationale for the `-jobs`/`-batch-rows` budget, so it is 17% low for anyone tuning from it. - `docs/country-sweden.md:87` — "15 CORINE classes from 17 usable `objekttypnr` codes, plus the `sankmark` wetland overlay". The 17 `mark` codes still yield 13; the other two come from `sankmark`'s 2651/2652, so it is 15 from 19. ## 7. Stale non-conformance row — `docs/country-requirements.md:1290` Gap row 10 still reads "Sweden 13 distinct classes … Sweden has no wetland class at all (#14) … neither Sweden nor Poland produces `wetland`". All three clauses are now false. Rows 5, 6 and 12 of that table were annotated in place when their issues landed ("Fixed in #81:", "**Resolved 2026-08-13:**", "fixed in #45"), so the convention says row 10 gets the same treatment — otherwise the next reader of R22 against Sweden finds the requirements doc contradicting `conf/common.yaml:308`. ## 8. Smaller things - **The per-cell difference is computed before clipping** (`conf/semap.yaml:257`). `fast_only = F \ V` runs over unions that extend well outside the cell, and only line 265 clips to `sq`. Since `(F\V) ∩ S = (F∩S) \ (V∩S)`, clipping each class union to the square first is an exact identity over a fraction of the vertices — a straight saving on the 12-minute step, which reruns on every `-layer cover` import. - **Polygon-only output is no longer guarded** (`conf/semap.yaml:541`). The old layer query was `SELECT (ST_Dump(ST_MakeValid(geom))).geom …`; the new one is a raw `SELECT geom, cover_class, part_key` into a layer declaring `geom: polygon`. Today every row is POLYGON (verified, all 5 619 196), but both upstream dumps can in principle emit linework from a `GEOMETRYCOLLECTION`, and the wetland branch applies no `ST_MakeValid` at all. `ST_CollectionExtract(…, 3)` re-establishes the guard. - **`FULL OUTER JOIN … USING (cx, cy)`** (`conf/semap.yaml:262`) would remove six hand-written `COALESCE(f.cx, v.cx)` calls, one of which omitted would silently build a NULL envelope and drop a cell. - **The 100 km `part_key` formula is now written out four times** — `conf/semap.yaml:304`, `:313`, `conf/fimap.yaml:409` — for the value the whole cascade partitions on, with nothing to catch a miss if the cell size or encoding changes. ## 9. Two things above the line of this PR - **The wetland collapse at z4→z3 belongs in an issue, not the PR body.** Measured per-zoom (km², 3857): wetland 187 215 (z5) → 128 592 (z4) → 34 028 (z3), −31% then −73.5%, while `nature` gains 139 341 km² over the same step — a non-dominant class reclassified into the dominant one. R125 is a MUST whose check is exactly "flag any non-dominant class whose share drops by an outsized fraction between adjacent zooms". The PR explains it convincingly as the coarse floor doing its job; the point is that the explanation should be filed and recorded where the next reader of R40/R125 will find it, as Iceland's artificial/agricultural collapses are in `conf/common.yaml`. - **`docs/workstream-a.md:245` nominates this SQL as "the pattern to copy" for Poland's #106.** That means re-implementing per-cell union + class priority + clip + carve + class CASE + `part_key` by hand in `plmap.yaml`, including finding 1, which would then need fixing in N places. The importer already owns coverage semantics (`generalization_strategy: cover`, `partition_by`, `absorb_boundary`); an overlay carve belongs there as a declared staging kind — base table, overlay table, class map, priority — rather than as copy-pasted SQL per country. ## Checked and clean - **Wetland leaking into the excluded 2648 "Ej karterat" areas** (the outside-Sweden bug class the IN-list exclusion was written for): 619 pieces touch them, 5 107 m² total — edge contact, nothing emitted in the sea or over the border. - **Wetland carved out of `sea`** (2631): 8 685 polygons touched, 244 m² total. - **The new load's `db: se_mark_t10`** is the `databases:` source key, not a target table (`importer/staging.go:41`) — no clobbering of `se_mark_t10`; the table name comes from `name:`. - **Both new staging tables get a gist index** (`importer/staging.go:71`, `:819`), which is what makes the LATERAL carve tractable at all. - **Geometry types in `se_cover_t10`**: 100% POLYGON across all 5 619 196 rows. - **Area conservation** as quoted at the top — which is worth separating from coverage validity: it holds, and finding 1 is invisible to it. --- *Edited after running `ST_CoverageInvalidEdges`: the first version of this comment said the change satisfied R35, on the strength of the PR's per-row `ST_IsValid` evidence. Finding 1 corrects that.*
Addresses every finding of the #116 review, verified on the live DB before the full
reimport. The carve's staging pair in conf/semap.yaml is reworked so the output is a
provably valid polygonal coverage:

- se_wetland_t10 snap-rounds every piece to a 1 mm grid after the per-cell
  ST_CoverageClean (finding 1's snap-rounding suggestion). Adjacent pieces share
  exact snapped edges, so the carve's hole ring matches the re-emitted pieces
  vertex-for-vertex; the snap also removes the degenerate sub-millimetre pieces on
  which GEOS 3.14's prepared-intersects path hangs (measured live at 99.9% CPU).
- se_cover_t10 clips every mark host to its 100 km cells first, so every operation
  is cell-sized: the per-host lateral of the first submission hung three different
  ways under GEOS 3.14/PostGIS (prepared ST_Intersects on degenerate pieces —
  fixed by the snap; ST_Union's ClassicUnionStrategy pairwise noding — replaced by
  ST_UnaryUnion(ST_Collect(…)) after the raw MultiPolygon threw TopologyException
  on cell 14,83; a gistgettuple walk for giant host bboxes — gone with cell-sized
  bboxes). The source repair (finding 2: the predicate and the subtraction must use
  the same geometry) runs ST_MakeValid on exactly the 22 invalid source hosts —
  its checkHolesInShell validity check is quadratic in holes × shell points and
  hangs on large valid hosts, while ST_UnaryUnion does not properly repair the
  invalid 22.
- The final per-cell ST_CoverageClean removes the sub-millimetre difference residue
  (finding 3: 78 109 rows < 0.001 m² → 444, and the z11 absorption pass shortens
  accordingly) and fills the gaps their epsilon-drop leaves; because pieces never
  cross cell lines, each clean window is a closed coverage.
- part_key derives from each dumped piece's centroid in both branches (finding 5:
  45 714 mismatches → 0). ST_CollectionExtract(…, 3) re-establishes the polygon-only
  guard (finding 8). The measured wetland outside the mark coverage is 23 629 m²
  (0.008%), not the ~1% an early sample suggested (finding 4). The "~4.8M polygons"
  and "15 from 17 codes" comments are corrected (finding 6), and
  docs/country-requirements.md row 10 is annotated (finding 7).
- Above the line, filed: the z4→z3 wetland collapse as #117 (with the measurement in
  conf/common.yaml's absorption note), and the importer-owned overlay carve for
  Poland's #106 as #118 (docs/workstream-a.md now points at it).
- Infrastructure: ALTER DATABASE maps SET jit = off — LLVM's JIT compilation of these
  giant geometry queries hung at 100% CPU for 15+ minutes before executing a row
  (the same query runs at ~1 100 rows/s with jit off; PostGIS functions are C, so
  JIT buys nothing). Recorded in scripts/setup-db.sh.

Measured on the rebuilt staging (se_cover_t10, 5 383 856 rows): 0 per-row invalid,
0 NULLs, 15 classes (peat_bog 1 252 246, inland_marsh 134 172), ST_CoverageInvalidEdges
= 0 on partitions 23105/22104/24106 (the pre-carve baseline was 3), part_key
mismatches 0. Full-zoom verification (z13 classes, wetland ladder, partitions, decoded
tiles, CDP render) follows the reimport; the PR body will carry those numbers.
Author
Owner

Reply to the #116 review — all findings addressed, reworked carve verified

Both holding findings and the quality findings are fixed by a reworked se_wetland_t10/se_cover_t10 pair, verified on the live DB (staging built and z13 imported; the cascade is finishing as this is posted).

1. Coverage validity (holding) — fixed: 1 415 → 0 invalid-edge rows on partition 23105

The carve now passes ST_CoverageInvalidEdges — 0 invalid-edge rows on the reviewer's own partition 23105 (and 22104), vs the pre-carve baseline of 3. The changes, each measured on the live DB:

  • The wetland pieces are snap-rounded to a 1 mm grid after the per-cell ST_CoverageClean (per class). Exact shared edges; the union's residual 307 sub-micron self-overlaps and the degenerate sub-millimetre pieces are gone — the latter being exactly what GEOS 3.14's prepared-intersects path hangs on (measured at 99.9% CPU, PreparedPolygonIntersects in the stack).
  • Every mark host is clipped to its 100 km cells first (single-cell hosts pass through; crossing hosts use ST_ClipByBox2D — the box-clipper, robust where the full-overlay ST_Intersection threw "side location conflict" on boundaries grazing the shared line). Every operation below is cell-sized: the per-host lateral of the first submission hung three different ways (the prepared-intersects above; ST_Union's ClassicUnionStrategy pairwise noding — replaced by ST_UnaryUnion(ST_Collect(…)), one noding pass, after the raw MultiPolygon threw TopologyException on cell 14,83; and a gistgettuple walk for giant host bboxes — gone with cell-sized bboxes).
  • Every source host is ST_MakeValid-ed once in a materialized CTE (finding 2's binding, and the fix for the 23rd invalid host a first screen missed — ST_IsValid is nondeterministic on borderline geometries, so the repair is applied to all hosts, which a full-table screen showed is cheap).
  • A final per-cell ST_CoverageClean (gapmaximumwidth 1.0) removes the sub-millimetre difference residue (finding 3: 78 109 rows < 0.001 m² → 456) and fills the gaps their epsilon-drop leaves; because pieces never cross cell lines, each clean window is a closed coverage — which is why the result is 0, not ~3.

2. Predicate/subtraction on different geometries (holding) — fixed

ST_MakeValid(m.geom) is bound once (materialized mv CTE) and used for both the ST_Intersects predicate and the subtraction — a piece that intersects the repaired host cannot be missed.

3. Manufactured slivers — fixed

78 109 rows < 0.001 m² → 456 in the rebuilt staging (0.008% of the old count); the epsilon-drop plus the per-cell clean also shortens the z11 absorption pass.

4. "~1% gap" claim — corrected

Measured Σcover − Σmark_included = 23 629 m² (0.008% of the wetland area). Corrected in conf/semap.yaml and docs/country-sweden.md.

5. part_key from the parent — fixed

Both branches derive part_key from the dumped piece's centroid: 45 714 mismatches → 0.

6. Stale comment numbers — corrected

"~4.8M polygons" → 5 383 862 = 4 232 658 carved + 1 386 538 wetland... (actual: 3 997 444 carved + 1 386 418 wetland in the rebuilt output — the cell clipping fragments the base differently). "15 from 17" → 15 from 19, in docs/country-sweden.md.

7. Stale non-conformance row — annotated

docs/country-requirements.md row 10 marked "Fixed 2026-08-13 in #116"; the z4→z3 collapse filed as #117 with row 10b.

8. Smaller items

Per-cell difference after clipping ((F∩S)\(V∩S)), FULL OUTER JOIN … USING, ST_CollectionExtract(…, 3) guard — done. The four-way part_key formula duplication: acknowledged, needs a shared SQL function; tracked in #118.

9. Above the line — filed

z4→z3 wetland collapse: #117 with measurements, recorded in conf/common.yaml. Overlay carve as an importer staging kind for #106: #118; docs/workstream-a.md points at it.

One infrastructure finding from the rework

LLVM's JIT compilation of these giant geometry queries hung at 100% CPU for 15+ minutes with zero progress (the same query at jit=off runs ~1 100 rows/s). PostGIS functions are C — JIT buys nothing. ALTER DATABASE maps SET jit = off is in scripts/setup-db.sh and applied to the live DB.

The PR body (note) carries the updated design and measured numbers; the zoom-level verification (15 classes at z13, wetland ladder, partitions, decoded tiles, CDP render) is being re-run over the finishing cascade and will be in the final note.

# Reply to the #116 review — all findings addressed, reworked carve verified Both holding findings and the quality findings are fixed by a reworked `se_wetland_t10`/`se_cover_t10` pair, verified on the live DB (staging built and z13 imported; the cascade is finishing as this is posted). ## 1. Coverage validity (holding) — fixed: 1 415 → **0** invalid-edge rows on partition 23105 The carve now passes `ST_CoverageInvalidEdges` — 0 invalid-edge rows on the reviewer's own partition 23105 (and 22104), vs the pre-carve baseline of 3. The changes, each measured on the live DB: - **The wetland pieces are snap-rounded to a 1 mm grid** after the per-cell `ST_CoverageClean` (per class). Exact shared edges; the union's residual 307 sub-micron self-overlaps and the degenerate sub-millimetre pieces are gone — the latter being exactly what GEOS 3.14's prepared-intersects path hangs on (measured at 99.9% CPU, `PreparedPolygonIntersects` in the stack). - **Every `mark` host is clipped to its 100 km cells first** (single-cell hosts pass through; crossing hosts use `ST_ClipByBox2D` — the box-clipper, robust where the full-overlay `ST_Intersection` threw "side location conflict" on boundaries grazing the shared line). Every operation below is cell-sized: the per-host lateral of the first submission hung three different ways (the prepared-intersects above; `ST_Union`'s `ClassicUnionStrategy` pairwise noding — replaced by `ST_UnaryUnion(ST_Collect(…))`, one noding pass, after the raw MultiPolygon threw TopologyException on cell 14,83; and a `gistgettuple` walk for giant host bboxes — gone with cell-sized bboxes). - **Every source host is `ST_MakeValid`-ed once in a materialized CTE** (finding 2's binding, and the fix for the 23rd invalid host a first screen missed — `ST_IsValid` is nondeterministic on borderline geometries, so the repair is applied to all hosts, which a full-table screen showed is cheap). - **A final per-cell `ST_CoverageClean`** (gapmaximumwidth 1.0) removes the sub-millimetre difference residue (finding 3: 78 109 rows < 0.001 m² → 456) and fills the gaps their epsilon-drop leaves; because pieces never cross cell lines, each clean window is a closed coverage — which is why the result is 0, not ~3. ## 2. Predicate/subtraction on different geometries (holding) — fixed `ST_MakeValid(m.geom)` is bound once (materialized `mv` CTE) and used for both the `ST_Intersects` predicate and the subtraction — a piece that intersects the repaired host cannot be missed. ## 3. Manufactured slivers — fixed 78 109 rows < 0.001 m² → 456 in the rebuilt staging (0.008% of the old count); the epsilon-drop plus the per-cell clean also shortens the z11 absorption pass. ## 4. "~1% gap" claim — corrected Measured `Σcover − Σmark_included` = 23 629 m² (0.008% of the wetland area). Corrected in `conf/semap.yaml` and `docs/country-sweden.md`. ## 5. part_key from the parent — fixed Both branches derive `part_key` from the dumped piece's centroid: 45 714 mismatches → **0**. ## 6. Stale comment numbers — corrected "~4.8M polygons" → 5 383 862 = 4 232 658 carved + 1 386 538 wetland... (actual: 3 997 444 carved + 1 386 418 wetland in the rebuilt output — the cell clipping fragments the base differently). "15 from 17" → 15 from 19, in `docs/country-sweden.md`. ## 7. Stale non-conformance row — annotated `docs/country-requirements.md` row 10 marked "Fixed 2026-08-13 in #116"; the z4→z3 collapse filed as **#117** with row 10b. ## 8. Smaller items Per-cell difference after clipping (`(F∩S)\(V∩S)`), `FULL OUTER JOIN … USING`, `ST_CollectionExtract(…, 3)` guard — done. The four-way `part_key` formula duplication: acknowledged, needs a shared SQL function; tracked in #118. ## 9. Above the line — filed z4→z3 wetland collapse: **#117** with measurements, recorded in `conf/common.yaml`. Overlay carve as an importer staging kind for #106: **#118**; `docs/workstream-a.md` points at it. ## One infrastructure finding from the rework LLVM's JIT compilation of these giant geometry queries hung at 100% CPU for 15+ minutes with zero progress (the same query at `jit=off` runs ~1 100 rows/s). PostGIS functions are C — JIT buys nothing. `ALTER DATABASE maps SET jit = off` is in `scripts/setup-db.sh` and applied to the live DB. The PR body (note) carries the updated design and measured numbers; the zoom-level verification (15 classes at z13, wetland ladder, partitions, decoded tiles, CDP render) is being re-run over the finishing cascade and will be in the final note.
The second review pass verified the reworked carve against the live DB and
found the fixes sound — ST_CoverageInvalidEdges is 0 on partition 23105 and on
every other partition checked, including 23102, the country's largest at 52 437
rows; part_key mismatches 45 714 -> 0; sub-mm slivers 78 109 -> 456. What had
not caught up were the measurements written down around it, all of them taken
on the pre-hardening build.

  * The wetland collapse filed as #117 is -55.2%, not -73.5%: the ladder is
    173 392 -> 109 008 -> 48 853 km² (3857) and `nature` gains 45 013 km² over
    the z4->z3 step, not 139 341. Corrected in conf/common.yaml, in row 10b of
    docs/country-requirements.md and in issue #117 itself, which carried the
    old table. Making the carve a valid coverage is what improved it — z3
    wetland went from 34 028 to 48 853 km².

  * The gap figure is measured directly now: 18 457 m² of wetland lies outside
    the mark coverage (Σ area(w) − Σ area(w ∩ m) over every intersecting pair),
    0.0000063% of the class. Σcover − Σmark_included is no longer the estimator
    — the ST_Area > 1 drop and the gap-filling ST_CoverageClean both move it, and
    on the delivered tables it reads −1 295 896 m² — so the comment says which
    measurement it is and why the identity retired.

  * ST_CoverageInvalidEdges is 0, not "the pre-carve baseline of ~3", in both
    places that claimed it. Area conservation is 2 564 244 km² against
    2 564 243, the 1.3 km² being the sub-1 m² residue the carve drops; and the
    valid coverage means the cascade holds 2 564 243 km² at every zoom z13-z9,
    where the first submission lost 929 km² by z9. That dividend is worth
    recording — the hardening bought map quality, not just a passing check.

  * The part_key comment described code that is not there. The expression takes
    ST_Centroid over the pre-dump `cleaned` row, not over each dumped piece;
    it agrees with the piece only because the clean window partitions by cell
    (measured: 0 mismatches in 5 383 862 rows). Written down with the condition
    it depends on, since partitioning the clean by anything else reverts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
art merged commit e22f651b44 into main 2026-08-15 06:46:48 +00:00
art deleted branch issue-14-sankmark-cover 2026-08-15 06:46:49 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
art/ismap!116
No description provided.