#127: tiles under the 0.5 MB budget — speckle floors, then a 1024-unit grid; heaviest tile 2.3 MB → 0.43 MB #143

Merged
art merged 3 commits from issue-127-tile-weight into main 2026-08-16 22:31:38 +00:00
Owner

Closes #127


Two passes. The first drops the features that paint nothing; the second stops
spending bytes on precision no screen can show. Neither alone reaches the issue's
0.5 MB — together, the heaviest tile anywhere on the map is 425 769 B, and a
full scan of all 3 487 non-empty tiles in z3–z9 finds none over 512 kB.

Pass 1 — the coarse cover speckle (ad7f16f's parent, 5a37b7e)

The heavy tiles were cover at z3–z7, and the weight was the coarse cascade's
invisible speckle: the issue-#41 cluster floor preserves "regions made of specks"
(Stockholm's urban fragments, the farmland belt, the mire regions) by carrying every
sub-pixel piece individually, so a z5 tile served the whole country at z13-ish
granularity. Measured on the z5 Norrland tile (17,8) before the pass: 68 044 features
/ 2 329 141 B, of which 98 % painted no pixel at the rendered 256×256 (decoded the
MVT and rasterized it) — the wetland carve of #14 was worst, 39 246 sub-pixel mire
pieces in that one tile. The z3 (4,2), z4 (8,4), z6 (35,16) and z7 (70,32) tiles were
the same disease at other scales.

The fix is a one-screen-pixel speckle floor on each country's cover layer
(zoom_filters, z3–z7): pieces smaller than pixelSizeAtZoom(z)² — the same
threshold the absorption no-neighbour drop rule already uses — are dropped at build
time, so a tile carries only what can draw a pixel. z4/z3 add a 30 km² (3857) ground
floor for wetland so the mire region still reaches z3 and R125 stays green (the
one-pixel floors there, 95.7/382.8 km², would drop it). z8 is untouched (already
≤ 325 kB, and it keeps the #41 urban fragments at the zoom where the city is drawn).

The render barely moves at 1×: rasterizing the new z5 (17,8) tile, nature paints
39 797 of 40 295 px (98.8 %), water 8 864 of 9 037 (98.1 %), agricultural 92 %; the
wetland texture thins from 2 835 to 1 842 px (35 %) — the sub-pixel mire speckles are
gone, the mire regions remain. The dropped pieces are below one screen pixel by
construction, so the holes they open are the same size the drop rule already accepts.

Pass 2 — the tile's coordinate grid (61ceeb2)

The floors got the feature count down (68 044 → 2 226 in the Norrland tile) and
stopped at 0.79 MB. What was left was not features, it was coordinates: 2 226 features
carrying 331 937 vertices at 2.38 bytes each — five vertices for every pixel a
256-pixel tile has, and still 1.3 per pixel of the 512 MapLibre actually draws it into.

Those vertices were not detail, they were precision. MVT stores coordinates as integers
on a grid extent units across the tile and the client rescales that grid to however
many pixels it draws the tile into; the extent is a precision choice and nothing else.
We asked for 4096 over a tile MapLibre draws at 512 CSS pixels (its default
tileSize for a vector source, which html/style.json does not override) — eight units
per pixel — underneath geometry the cascade had already simplified with a tolerance of
pixelSizeAtZoom(z)/2, which, pixelSizeAtZoom counting 256-pixel tiles, is one whole
CSS pixel. Three bits per coordinate describing wiggles the simplifier had already ruled
invisible.

mvtExtent = 1024 is two units per CSS pixel, one per device pixel at 2×. Snapping
to it moves no vertex by more than a quarter of a CSS pixel per axis, and it is the only
vertex-dropping that is safe on a coverage: the grid is a function of position alone,
so both sides of a shared boundary land on the same grid point and no gaps open.
Simplifying the polygons one at a time — the alternative the old "why 0.5 MB is not
reached" section of this PR proposed — would tear cover open. mvtBuffer = 64 keeps
the bleed at the same 1/16 of the extent #37 settled on, so no feature enters or leaves
a tile; only where its coordinates land on it changes.

ST_AsMVT's extent is a separate argument from ST_AsMVTGeom's and both default
to 4096. The first version of this set only ST_AsMVTGeom's: every function ran, every
tile decoded, tiles --check passed, and the map drew at quarter scale in the top-left
corner of each tile. Three things stop that now — mvtAggCall carries the extent at
every call site, TestMvtExtentIsDeclaredEverywhere pins both, and tiles --check
asserts the extent the tile declares (and that no feature arrives without a geometry,
which is what the new WHERE q.geom IS NOT NULL filters out: three sub-unit polygons in
the Norrland tile).

Before → after → after (cover layer, importer tiles --size)

zoom tile before after floors after grid features before → after
z3 (4,2) ~1.5 MB (33 019 feats) 666 151 B 182 333 B → 2 424
z4 (8,4) ~1.5 MB+ (not in #139's 5–13 window) 905 112 B 255 788 B → 2 078
z5 (17,8) Norrland 2 329 141 B 791 395 B 333 315 B 68 044 → 2 223
z5 (17,9) Stockholm 1 845 800 B 681 722 B 269 383 B → 1 511
z6 (35,16) 1 605 407 B 567 733 B 371 478 B 46 419 → 2 921
z7 (70,32) 660 134 B 314 396 B 255 053 B 17 471 → 2 732
z8 (141,86) 325 093 B 325 093 B 285 017 B unchanged by the floors

And the whole tile (zxy_map, every layer), heaviest tile at each zoom from a full
scan of all 3 487 non-empty tiles in z3–z9:

zoom before after zoom before after
z3 752 545 234 892 z7 378 272 345 287
z4 976 028 301 275 z8 387 196 339 113
z5 847 877 379 882 z9 386 095 355 070
z6 599 307 425 769

Tiles over 512 kB: 7 → 0. z10–z15, 9×9 grids over the four capitals: ≤ 288 kB. All
layers other than cover were already ≤ 118 kB; the grid takes 4–17 % off them too.

Acceptance-test changes

Every tile-size test now asserts the issue's 512 kB budget rather than a runaway cap
around whatever the last measurement happened to be. A budget that tracks the
measurement is not a budget.

  • The z5 Stockholm tile test: 8 MB → 512 kB (measured 325 292 B).
  • Companion test on the other heavy z5 tile, Norrland (17,8): 512 kB (measured
    373 323 B). Named for the tile rather than "the heaviest z5 cover tile", which it
    stopped being after the pass — Warsaw's (17,10) is, at 379 882 B.
  • The per-country capital budgets: 8 MB at z5 / 2 MB at z8 → 512 kB at both
    (is 75 292 / 23 888, se 325 292 / 245 290, pl 379 918 / 227 181, fi 101 497 / 49 798).
  • The R38/R125 comments in conf/semap.yaml record the post-pass measurements:
    R38 reads all-negative growth z9→z3 (the z4/z3 floors drop the sub-pixel
    coastal pieces before they can cross into the sea, so the #47 coastline-eater
    cannot fire: −4 110 / −11 507 / −7 803 / −6 430 / −5 655 / −36 049 km²);
    R125's z3 wetland re-measures at 112 006 km² from 724 pieces (the thinned
    coarse tables now fit one processing batch, so the cluster floor chains the mire
    region instead of the batch split eating it — comfortably above the 30 000 km²
    bound, and the z4/z3 wetland floor exists exactly so this stays true).

Two comment corrections came out of reviewing pass 1 (ad7f16f and part of 61ceeb2):
every country's speckle-floor comment said the filters act on z5–z7 when they are
z3–z7 — skipping exactly the two entries that carry the wetland ground floor — and the
post-pass byte counts recorded in conf/semap.yaml disagreed with this PR's own table
(the z4 entry by 3×, in the opposite direction). They are now stated as before → floors
→ grid through zxy_map_cover so the units cannot drift again.

Also here, from #139's tool

Extending tiles --check meant reading it: its docs named the pre-#129 flat flags
(-tile-size-top, -tile-size-zooms, -tile-bench-funcs) the subcommand rewrite
removed, and both the doc and the code said the probe assigns a feature to the tile of
its centroid when the SQL uses the north-west corner of its bounding box. Fixed here
because this branch already touches the file; the rest of the #139 review is a comment
on #142.

Verification

  • make test — green, plus TestMvtExtentIsDeclaredEverywhere.
  • importer tiles --check for every config (ismap/semap/plmap/fimap/world) — R121
    PASS, cover keys [cover_class country] unchanged, extent and geometry assertions
    green (and verified to FAIL when the extent is deliberately half-applied).
  • Acceptance, all configs: is 31/31, se 32/32 (with --acceptance all, incl. the 100 s
    R35 carve check), pl 29/29, fi 34/34, world 14/14.
  • Reimports for pass 1 were per-layer --regeneralize 3-7 (cover only), all four
    countries; no maxz re-import, no fetch, no destructive ops. Pass 2 changed no data —
    it is generated SQL, reinstalled by ensureParentSQL on the next import of any config.
    (Iceland's own five layers are per-country, so their functions regenerate when those
    layers are imported. For verification the DDL was installed from the importer's own
    generators, so the live DB is on the new grid.)
  • Real-browser A/B at z5 / z6 / z12 / z18, at 1× and 2× device pixel ratio, plus
    Iceland, Poland and Finland renders: indistinguishable at the size the map is drawn.
    At 5× magnification the quarter-pixel quantization is visible, which is the honest way
    to describe it.

Branch / dependency

Branch issue-127-tile-weight, on top of origin/issue-139-tile-tool
(PR #142, the tile tool this pass measures with — merge order matters: #142 first,
then this). The diff is only the #127 work.

# Closes #127 --- Two passes. The first drops the features that paint nothing; the second stops spending bytes on precision no screen can show. Neither alone reaches the issue's 0.5 MB — together, **the heaviest tile anywhere on the map is 425 769 B**, and a full scan of all 3 487 non-empty tiles in z3–z9 finds none over 512 kB. ## Pass 1 — the coarse cover speckle (`ad7f16f`'s parent, `5a37b7e`) The heavy tiles were `cover` at z3–z7, and the weight was the coarse cascade's **invisible speckle**: the issue-#41 cluster floor preserves "regions made of specks" (Stockholm's urban fragments, the farmland belt, the mire regions) by carrying every sub-pixel piece individually, so a z5 tile served the whole country at z13-ish granularity. Measured on the z5 Norrland tile (17,8) before the pass: 68 044 features / 2 329 141 B, of which **98 % painted no pixel** at the rendered 256×256 (decoded the MVT and rasterized it) — the wetland carve of #14 was worst, 39 246 sub-pixel mire pieces in that one tile. The z3 (4,2), z4 (8,4), z6 (35,16) and z7 (70,32) tiles were the same disease at other scales. The fix is a **one-screen-pixel speckle floor** on each country's `cover` layer (`zoom_filters`, z3–z7): pieces smaller than `pixelSizeAtZoom(z)²` — the same threshold the absorption no-neighbour drop rule already uses — are dropped at build time, so a tile carries only what can draw a pixel. z4/z3 add a 30 km² (3857) ground floor for **wetland** so the mire region still reaches z3 and R125 stays green (the one-pixel floors there, 95.7/382.8 km², would drop it). z8 is untouched (already ≤ 325 kB, and it keeps the #41 urban fragments at the zoom where the city is drawn). **The render barely moves at 1×**: rasterizing the new z5 (17,8) tile, nature paints 39 797 of 40 295 px (98.8 %), water 8 864 of 9 037 (98.1 %), agricultural 92 %; the wetland texture thins from 2 835 to 1 842 px (35 %) — the sub-pixel mire speckles are gone, the mire regions remain. The dropped pieces are below one screen pixel by construction, so the holes they open are the same size the drop rule already accepts. ## Pass 2 — the tile's coordinate grid (`61ceeb2`) The floors got the **feature count** down (68 044 → 2 226 in the Norrland tile) and stopped at 0.79 MB. What was left was not features, it was coordinates: 2 226 features carrying **331 937 vertices at 2.38 bytes each** — five vertices for every pixel a 256-pixel tile has, and still 1.3 per pixel of the 512 MapLibre actually draws it into. Those vertices were not detail, they were precision. MVT stores coordinates as integers on a grid `extent` units across the tile and the client rescales that grid to however many pixels it draws the tile into; the extent is a precision choice and nothing else. We asked for 4096 over a tile MapLibre draws at **512 CSS pixels** (its default `tileSize` for a vector source, which `html/style.json` does not override) — eight units per pixel — underneath geometry the cascade had already simplified with a tolerance of `pixelSizeAtZoom(z)/2`, which, `pixelSizeAtZoom` counting 256-pixel tiles, is one whole CSS pixel. Three bits per coordinate describing wiggles the simplifier had already ruled invisible. `mvtExtent = 1024` is **two units per CSS pixel**, one per device pixel at 2×. Snapping to it moves no vertex by more than a quarter of a CSS pixel per axis, and it is the only vertex-dropping that is **safe on a coverage**: the grid is a function of position alone, so both sides of a shared boundary land on the same grid point and no gaps open. Simplifying the polygons one at a time — the alternative the old "why 0.5 MB is not reached" section of this PR proposed — would tear `cover` open. `mvtBuffer = 64` keeps the bleed at the same 1/16 of the extent #37 settled on, so no feature enters or leaves a tile; only where its coordinates land on it changes. `ST_AsMVT`'s `extent` is a **separate argument** from `ST_AsMVTGeom`'s and both default to 4096. The first version of this set only `ST_AsMVTGeom`'s: every function ran, every tile decoded, `tiles --check` passed, and the map drew at quarter scale in the top-left corner of each tile. Three things stop that now — `mvtAggCall` carries the extent at every call site, `TestMvtExtentIsDeclaredEverywhere` pins both, and `tiles --check` asserts the extent the tile declares (and that no feature arrives without a geometry, which is what the new `WHERE q.geom IS NOT NULL` filters out: three sub-unit polygons in the Norrland tile). ## Before → after → after (`cover` layer, `importer tiles --size`) | zoom tile | before | after floors | after grid | features before → after | |---|---|---|---|---| | z3 (4,2) | ~1.5 MB (33 019 feats) | 666 151 B | **182 333 B** | → 2 424 | | z4 (8,4) | ~1.5 MB+ (not in #139's 5–13 window) | 905 112 B | **255 788 B** | → 2 078 | | z5 (17,8) Norrland | 2 329 141 B | 791 395 B | **333 315 B** | 68 044 → 2 223 | | z5 (17,9) Stockholm | 1 845 800 B | 681 722 B | **269 383 B** | → 1 511 | | z6 (35,16) | 1 605 407 B | 567 733 B | **371 478 B** | 46 419 → 2 921 | | z7 (70,32) | 660 134 B | 314 396 B | **255 053 B** | 17 471 → 2 732 | | z8 (141,86) | 325 093 B | 325 093 B | **285 017 B** | unchanged by the floors | And the whole tile (`zxy_map`, every layer), **heaviest tile at each zoom** from a full scan of all 3 487 non-empty tiles in z3–z9: | zoom | before | after | zoom | before | after | |---|---|---|---|---|---| | z3 | 752 545 | **234 892** | z7 | 378 272 | **345 287** | | z4 | 976 028 | **301 275** | z8 | 387 196 | **339 113** | | z5 | 847 877 | **379 882** | z9 | 386 095 | **355 070** | | z6 | 599 307 | **425 769** | | | | Tiles over 512 kB: **7 → 0**. z10–z15, 9×9 grids over the four capitals: ≤ 288 kB. All layers other than `cover` were already ≤ 118 kB; the grid takes 4–17 % off them too. ## Acceptance-test changes Every tile-size test now asserts **the issue's 512 kB budget** rather than a runaway cap around whatever the last measurement happened to be. A budget that tracks the measurement is not a budget. * The z5 Stockholm tile test: **8 MB → 512 kB** (measured 325 292 B). * Companion test on the other heavy z5 tile, Norrland (17,8): **512 kB** (measured 373 323 B). Named for the tile rather than "the heaviest z5 cover tile", which it stopped being after the pass — Warsaw's (17,10) is, at 379 882 B. * The per-country capital budgets: **8 MB at z5 / 2 MB at z8 → 512 kB at both** (is 75 292 / 23 888, se 325 292 / 245 290, pl 379 918 / 227 181, fi 101 497 / 49 798). * The R38/R125 comments in `conf/semap.yaml` record the post-pass measurements: R38 reads **all-negative** growth z9→z3 (the z4/z3 floors drop the sub-pixel coastal pieces before they can cross into the sea, so the #47 coastline-eater cannot fire: −4 110 / −11 507 / −7 803 / −6 430 / −5 655 / −36 049 km²); R125's z3 wetland re-measures at **112 006 km² from 724 pieces** (the thinned coarse tables now fit one processing batch, so the cluster floor chains the mire region instead of the batch split eating it — comfortably above the 30 000 km² bound, and the z4/z3 wetland floor exists exactly so this stays true). Two comment corrections came out of reviewing pass 1 (`ad7f16f` and part of `61ceeb2`): every country's speckle-floor comment said the filters act on **z5–z7** when they are z3–z7 — skipping exactly the two entries that carry the wetland ground floor — and the post-pass byte counts recorded in `conf/semap.yaml` disagreed with this PR's own table (the z4 entry by 3×, in the opposite direction). They are now stated as before → floors → grid through `zxy_map_cover` so the units cannot drift again. ## Also here, from #139's tool Extending `tiles --check` meant reading it: its docs named the pre-#129 flat flags (`-tile-size-top`, `-tile-size-zooms`, `-tile-bench-funcs`) the subcommand rewrite removed, and both the doc and the code said the probe assigns a feature to the tile of its **centroid** when the SQL uses the north-west corner of its bounding box. Fixed here because this branch already touches the file; the rest of the #139 review is a comment on #142. ## Verification * `make test` — green, plus `TestMvtExtentIsDeclaredEverywhere`. * `importer tiles --check` for every config (ismap/semap/plmap/fimap/world) — R121 PASS, cover keys `[cover_class country]` unchanged, extent and geometry assertions green (and verified to FAIL when the extent is deliberately half-applied). * Acceptance, all configs: is 31/31, se 32/32 (with `--acceptance all`, incl. the 100 s R35 carve check), pl 29/29, fi 34/34, world 14/14. * Reimports for pass 1 were per-layer `--regeneralize 3-7` (cover only), all four countries; no maxz re-import, no fetch, no destructive ops. **Pass 2 changed no data** — it is generated SQL, reinstalled by `ensureParentSQL` on the next import of any config. (Iceland's own five layers are per-country, so their functions regenerate when those layers are imported. For verification the DDL was installed from the importer's own generators, so the live DB is on the new grid.) * Real-browser A/B at z5 / z6 / z12 / z18, at **1× and 2× device pixel ratio**, plus Iceland, Poland and Finland renders: indistinguishable at the size the map is drawn. At 5× magnification the quarter-pixel quantization is visible, which is the honest way to describe it. ## Branch / dependency Branch **issue-127-tile-weight**, on top of **origin/issue-139-tile-tool** (PR #142, the tile tool this pass measures with — merge order matters: #142 first, then this). The diff is only the #127 work.
The heavy tiles were `cover` at z3–z7, and the weight was the coarse cascade's
invisible speckle: the #41 cluster floor preserves "regions made of specks" by
carrying every sub-pixel piece individually, so a z5 tile served the whole country
at z13-ish granularity — 68 044 features / 2 329 141 B in the Norrland tile (17,8),
of which 98% paint no pixel at the rendered 256×256 (decoded and rasterized the
MVT; the #14 wetland carve was worst, 39 246 sub-pixel mire pieces in that tile).

Each country's `cover` now carries a one-screen-pixel speckle floor (zoom_filters,
z3–z7): pieces below pixelSizeAtZoom(z)² — the same threshold the absorption
no-neighbour drop rule uses — are dropped at build time, so a tile carries only
what can draw. z4/z3 add a 30 km² (3857) ground floor for wetland so the mire
region still reaches z3 and R125 stays green (the one-pixel floors there would
drop it). z8 untouched: already ≤ 325 kB and it keeps the #41 urban fragments at
the zoom where the city is drawn.

Measured (`tiles --size`, heaviest per zoom): z3 (4,2) ~1.5 MB → 666 kB; z4 (8,4)
~1.5 MB+ → 905 kB; z5 (17,8) 2 329 141 → 791 395 B (68 044 → 2 226 features);
z5 (17,9) 1 845 800 → 681 722 B; z6 (35,16) 1 605 407 → 567 733 B; z7 (70,32)
660 134 → 314 396 B. Bench z5 grid max 2 385 623 → 847 877 B. The 1× render is
~unchanged (nature 98.8% of painted px, water 98.1%; wetland texture thins 35% —
the mire regions remain). Six coarse tiles still sit at 0.56–0.91 MB — the
VISIBLE land cover at the half-pixel tolerance; 0.5 MB needs the last-resort
tolerance knob ("simplify more") and is documented as pushback in the PR.

Acceptance: R38 now reads all-negative growth z9→z3 (the z4/z3 floors drop the
sub-pixel coastal pieces before they can cross into the sea — the #47 eater cannot
fire); R125 re-measures 112 006 km² of z3 wetland from 724 pieces (the thinned
tables fit one batch, so the cluster floor chains the mire region instead of the
batch split eating it — comfortably above the 30 000 km² bound). The z5 8 MB test
is tightened to 2 MB (measured 0.68 MB) and a companion 2 MB test pins the Norrland
tile. All acceptance green (is 31, se 32 incl. 100 s R35, pl 29, fi 34, world 14),
`tiles --check` R121 PASS for every config, make build && make test green, CDP
render check green.
art changed title from # Closes #127 to #127: drop the coarse cover speckle — z5 tile 2.3 MB → 0.79 MB, z3–z7 all 52–66% lighter 2026-08-16 10:48:11 +00:00
Every country's `cover` carries the same five zoom_filters, z3 through z7, and
the same comment above them saying they act on z5–z7. The z4 and z3 entries are
the interesting ones — they are the two with the wetland ground floor, and the
z3 one is what R125 turns on — so the comment skips exactly the part a reader
needs. Poland's copy also said "the z5/z6/z7 cover tiles".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The speckle floors got the FEATURE COUNT down (68 044 → 2 226 in the z5 Norrland
tile) and stopped at 0.79 MB, above the 0.5 MB the issue asked for. What was left
was not features, it was coordinates: 2 226 features carrying 331 937 vertices at
2.38 bytes each — five vertices for every pixel a 256-pixel tile has, and still
1.3 per pixel of the 512 MapLibre actually draws it into.

Those vertices were not detail, they were precision. MVT stores coordinates as
integers on a grid `extent` units across the tile and the client rescales that
grid to however many pixels it draws; extent is a precision choice and nothing
else. We asked for 4096 over a tile MapLibre draws at 512 CSS pixels — eight
units per pixel — underneath geometry the cascade had already simplified with a
tolerance of pixelSizeAtZoom(z)/2, which is one whole CSS pixel. Three bits per
coordinate describing wiggles the simplifier had already ruled invisible.

mvtExtent = 1024 is two units per CSS pixel, one per device pixel at 2x. Snapping
to it moves no vertex by more than a quarter of a CSS pixel per axis, and it is
the only vertex-dropping that is safe on a coverage: the grid is a function of
position alone, so both sides of a shared boundary land on the same grid point
and no gaps open. mvtBuffer = 64 keeps the bleed at the same 1/16 of the extent
issue #37 settled on, so no feature enters or leaves a tile — only where its
coordinates land on it changes.

  heaviest tile at each zoom — a full scan of all 3 487 non-empty tiles in z3–z9
    z3  752 545 → 234 892     z7  378 272 → 345 287
    z4  976 028 → 301 275     z8  387 196 → 339 113
    z5  847 877 → 379 882     z9  386 095 → 355 070
    z6  599 307 → 425 769
  tiles over 512 kB: 7 → 0. z10–z15, 9x9 grids over the four capitals: ≤ 288 kB.

Every tile-size acceptance test now asserts the issue's 512 kB budget instead of
a runaway cap around an obsolete measurement: the two z5 tests 2 MB → 512 kB, and
the per-country capital budgets 8 MB/2 MB → 512 kB.

ST_AsMVT's `extent` is a SEPARATE argument from ST_AsMVTGeom's and both default
to 4096. Setting one and not the other produces a tile that decodes perfectly,
passes the R121 key check, and draws at quarter scale in its top-left corner —
which is how the first version of this shipped to a browser. Three things now
stop that: mvtAggCall carries the extent at every call site,
TestMvtExtentIsDeclaredEverywhere pins both of them, and `tiles --check` asserts
the extent the tile declares (and that no feature arrives without a geometry,
which is what the new `WHERE q.geom IS NOT NULL` filters out — three sub-unit
polygons in the Norrland tile).

Also here, from reviewing #139's tool while extending it: its docs named the
pre-#129 flat flags (-tile-size-top, -tile-size-zooms, -tile-bench-funcs) that
the subcommand rewrite removed, and both the doc and the code said the probe
assigns a feature to the tile of its centroid when the SQL uses the north-west
corner of its bounding box.

Verified: make test; importer tiles --check for all five configs; acceptance for
all five (is 31/31, se 32/32 with --acceptance all, pl 29/29, fi 34/34, world
14/14); real-browser A/B at z5/z6/z12/z18 at 1x and 2x device pixel ratio, plus
Iceland/Poland/Finland renders — indistinguishable at the size the map is drawn.
No data changed: this is generated SQL, reinstalled by ensureParentSQL on the
next import of any config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator

Review — and two commits on the branch (ad7f16f, 61ceeb2)

The diagnosis is right and the mechanism is right: the #41 cluster floor really was
carrying a region's worth of sub-pixel specks individually, and dropping them at one
screen pixel is the correct rule. But the pass stopped at 0.79 MB against the issue's
0.5 MB, and it attributed the rest to "simplify more — the maintainer's call". That
framing is what I disagree with, and it is what the second commit fixes.

The remaining weight was precision, not detail

After the floors the z5 Norrland tile is 2 226 features carrying 331 937 vertices at
2.38 bytes each
— five vertices for every pixel a 256-pixel tile has, and still 1.3
per pixel of the 512 MapLibre draws it into. Those vertices were not detail the map
needed; they were sub-pixel coordinate precision.

MVT stores coordinates as integers on a grid extent units across the tile and the
client rescales that grid to however many pixels it draws the tile into — the extent is
a precision choice and nothing else. We asked for 4096 over a tile MapLibre draws at
512 CSS pixels (its default tileSize for a vector source, which html/style.json
does not override): eight units per pixel, underneath geometry the cascade had already
simplified with a tolerance of pixelSizeAtZoom(z)/2 — which, pixelSizeAtZoom
counting 256-pixel tiles, is one whole CSS pixel. Three bits per coordinate describing
wiggles the simplifier had already ruled invisible.

mvtExtent = 1024 is two units per CSS pixel, one per device pixel at 2×. Snapping to
it moves no vertex by more than a quarter of a CSS pixel per axis, and it is the only
vertex-dropping that is safe on a coverage
: the grid is a function of position alone,
so both sides of a shared boundary land on the same grid point and no gaps open.
Simplifying the polygons one at a time — the obvious alternative, and the one "simplify
more" implies — would tear cover open. mvtBuffer = 64 keeps the bleed at the same
1/16 of the extent #37 settled on, so no feature enters or leaves a tile; only where its
coordinates land on it changes.

Heaviest tile at each zoom, from a full scan of all 3 487 non-empty tiles in z3–z9:

zoom before after zoom before after
z3 752 545 234 892 z7 378 272 345 287
z4 976 028 301 275 z8 387 196 339 113
z5 847 877 379 882 z9 386 095 355 070
z6 599 307 425 769

Tiles over 512 kB: 7 → 0. z10–z15, 9×9 grids over the four capitals: ≤ 288 kB. Every
tile-size acceptance test now asserts the issue's 512 kB budget rather than a runaway cap
around an obsolete measurement (the two z5 tests 2 MB → 512 kB, the per-country capital
budgets 8 MB/2 MB → 512 kB).

One thing I got wrong first, worth knowing: ST_AsMVT's extent is a separate
argument from ST_AsMVTGeom's and both default to 4096. The first version set only
ST_AsMVTGeom's — every function ran, every tile decoded, tiles --check passed, and
the map drew at quarter scale in the corner of each tile. Three things stop that now:
mvtAggCall carries the extent at every call site, TestMvtExtentIsDeclaredEverywhere
pins both, and tiles --check asserts the extent the tile declares (see the #139 review).

Review findings on the pass itself

The measurements recorded in conf/semap.yaml contradict the PR body's own table.
The comment says z5 (17,8) 2 329 141 → 909 kB, z5 (17,9) 1 845 800 → 828 kB, z4 (8,4) ~1.5 MB → ~300 kB, z3 (4,2) ~1.5 MB → ~500 kB. The PR body says 791 395 / 681 722 /
905 112 / 666 151, and the z4 entry is off by 3× in the opposite direction. Measured
against the live DB, the comment is wrong on all four. Rewritten, and now stated as
three columns (before → floors → grid) through zxy_map_cover so the units cannot drift
again.

Every country's comment says the filters act on z5–z7; they are z3–z7. That skips
exactly the two entries with the wetland ground floor — the z3 one being what R125 turns
on. Poland's copy also said "the z5/z6/z7 cover tiles". Fixed in ad7f16f.

The new test is named "the heaviest z5 cover tile (Norrland 17/8)" and 17/8 was not
the heaviest z5 tile after the pass
— Warsaw's 17/10 was (379 882 against 373 323).
Renamed to say what the tile actually is; the pre-pass worst-offender history is kept in
the comment, which is the useful part.

Not fixed, your call: the four copy-pasted zoom_filters blocks. Twenty hardcoded
thresholds across four config files, and they are all just pixelSizeAtZoom(z)² — a
number the importer already computes. cover is a shared layer with a contract in
conf/common.yaml; if the contract could carry zoom_filters, this would be one block
instead of four and the thresholds could be derived instead of pasted. As it stands a
change to the floor has to be made identically in four places and nothing fails if one
is missed. That is a config-schema change, so I left it.

Worth a thought: R38 now reads negative on every coarse step, which the comment
presents as proof the #47 eater cannot fire. True — but it also means the test's upper
bound is no longer exercised anywhere near its value, and the thing that is now
happening to the map (water shrinking 36 049 km² across the z3 step alone) has no bound
on it at all. A lower bound may be the half of R38 with signal now. Untouched.

Verification

make test; importer tiles --check clean for all five configs; acceptance for all five
(is 31/31, se 32/32 with --acceptance all, pl 29/29, fi 34/34, world 14/14); real-browser
A/B at z5/z6/z12/z18 at 1× and 2× device pixel ratio plus Iceland/Poland/Finland renders —
indistinguishable at the size the map is drawn (at 5× magnification the quantization is
visible, which is the honest way to describe a quarter-pixel grid).

No data changed: this is generated SQL, reinstalled by ensureParentSQL on the next
import of any config. The Iceland-only layers (contours, peaks, buildings,
airports, airport_points) are per-country, so their functions regenerate only when
those layers are imported. For verification I installed the DDL by hand from the
importer's own generators, so the live DB is on the new grid now.

## Review — and two commits on the branch (`ad7f16f`, `61ceeb2`) The diagnosis is right and the mechanism is right: the #41 cluster floor really was carrying a region's worth of sub-pixel specks individually, and dropping them at one screen pixel is the correct rule. But the pass stopped at 0.79 MB against the issue's 0.5 MB, and it attributed the rest to "simplify more — the maintainer's call". That framing is what I disagree with, and it is what the second commit fixes. ### The remaining weight was precision, not detail After the floors the z5 Norrland tile is 2 226 features carrying **331 937 vertices at 2.38 bytes each** — five vertices for every pixel a 256-pixel tile has, and still 1.3 per pixel of the 512 MapLibre draws it into. Those vertices were not detail the map needed; they were sub-pixel *coordinate precision*. MVT stores coordinates as integers on a grid `extent` units across the tile and the client rescales that grid to however many pixels it draws the tile into — the extent is a precision choice and nothing else. We asked for 4096 over a tile MapLibre draws at **512 CSS pixels** (its default `tileSize` for a vector source, which `html/style.json` does not override): eight units per pixel, underneath geometry the cascade had already simplified with a tolerance of `pixelSizeAtZoom(z)/2` — which, `pixelSizeAtZoom` counting 256-pixel tiles, is one whole CSS pixel. Three bits per coordinate describing wiggles the simplifier had already ruled invisible. `mvtExtent = 1024` is two units per CSS pixel, one per device pixel at 2×. Snapping to it moves no vertex by more than a quarter of a CSS pixel per axis, and it is the **only vertex-dropping that is safe on a coverage**: the grid is a function of position alone, so both sides of a shared boundary land on the same grid point and no gaps open. Simplifying the polygons one at a time — the obvious alternative, and the one "simplify more" implies — would tear `cover` open. `mvtBuffer = 64` keeps the bleed at the same 1/16 of the extent #37 settled on, so no feature enters or leaves a tile; only where its coordinates land on it changes. Heaviest tile at each zoom, from a full scan of all 3 487 non-empty tiles in z3–z9: | zoom | before | after | zoom | before | after | |---|---|---|---|---|---| | z3 | 752 545 | **234 892** | z7 | 378 272 | **345 287** | | z4 | 976 028 | **301 275** | z8 | 387 196 | **339 113** | | z5 | 847 877 | **379 882** | z9 | 386 095 | **355 070** | | z6 | 599 307 | **425 769** | | | | Tiles over 512 kB: **7 → 0**. z10–z15, 9×9 grids over the four capitals: ≤ 288 kB. Every tile-size acceptance test now asserts the issue's 512 kB budget rather than a runaway cap around an obsolete measurement (the two z5 tests 2 MB → 512 kB, the per-country capital budgets 8 MB/2 MB → 512 kB). **One thing I got wrong first, worth knowing:** `ST_AsMVT`'s `extent` is a separate argument from `ST_AsMVTGeom`'s and both default to 4096. The first version set only `ST_AsMVTGeom`'s — every function ran, every tile decoded, `tiles --check` passed, and the map drew at quarter scale in the corner of each tile. Three things stop that now: `mvtAggCall` carries the extent at every call site, `TestMvtExtentIsDeclaredEverywhere` pins both, and `tiles --check` asserts the extent the tile declares (see the #139 review). ### Review findings on the pass itself **The measurements recorded in `conf/semap.yaml` contradict the PR body's own table.** The comment says `z5 (17,8) 2 329 141 → 909 kB, z5 (17,9) 1 845 800 → 828 kB, z4 (8,4) ~1.5 MB → ~300 kB, z3 (4,2) ~1.5 MB → ~500 kB`. The PR body says 791 395 / 681 722 / 905 112 / 666 151, and the z4 entry is off by 3× in the *opposite* direction. Measured against the live DB, the comment is wrong on all four. Rewritten, and now stated as three columns (before → floors → grid) through `zxy_map_cover` so the units cannot drift again. **Every country's comment says the filters act on z5–z7; they are z3–z7.** That skips exactly the two entries with the wetland ground floor — the z3 one being what R125 turns on. Poland's copy also said "the z5/z6/z7 cover tiles". Fixed in `ad7f16f`. **The new test is named "the heaviest z5 cover tile (Norrland 17/8)" and 17/8 was not the heaviest z5 tile after the pass** — Warsaw's 17/10 was (379 882 against 373 323). Renamed to say what the tile actually is; the pre-pass worst-offender history is kept in the comment, which is the useful part. **Not fixed, your call: the four copy-pasted `zoom_filters` blocks.** Twenty hardcoded thresholds across four config files, and they are all just `pixelSizeAtZoom(z)²` — a number the importer already computes. `cover` is a shared layer with a contract in `conf/common.yaml`; if the contract could carry `zoom_filters`, this would be one block instead of four and the thresholds could be derived instead of pasted. As it stands a change to the floor has to be made identically in four places and nothing fails if one is missed. That is a config-schema change, so I left it. **Worth a thought: R38 now reads negative on every coarse step**, which the comment presents as proof the #47 eater cannot fire. True — but it also means the test's upper bound is no longer exercised anywhere near its value, and the thing that *is* now happening to the map (water shrinking 36 049 km² across the z3 step alone) has no bound on it at all. A lower bound may be the half of R38 with signal now. Untouched. ### Verification `make test`; `importer tiles --check` clean for all five configs; acceptance for all five (is 31/31, se 32/32 with `--acceptance all`, pl 29/29, fi 34/34, world 14/14); real-browser A/B at z5/z6/z12/z18 at 1× and 2× device pixel ratio plus Iceland/Poland/Finland renders — indistinguishable at the size the map is drawn (at 5× magnification the quantization is visible, which is the honest way to describe a quarter-pixel grid). No data changed: this is generated SQL, reinstalled by `ensureParentSQL` on the next import of any config. The Iceland-only layers (`contours`, `peaks`, `buildings`, `airports`, `airport_points`) are per-country, so their functions regenerate only when those layers are imported. For verification I installed the DDL by hand from the importer's own generators, so the live DB is on the new grid now.
art-bot changed title from #127: drop the coarse cover speckle — z5 tile 2.3 MB → 0.79 MB, z3–z7 all 52–66% lighter to #127: tiles under the 0.5 MB budget — speckle floors, then a 1024-unit grid; heaviest tile 2.3 MB → 0.43 MB 2026-08-16 22:25:58 +00:00
art changed target branch from issue-139-tile-tool to main 2026-08-16 22:31:26 +00:00
art merged commit 61ceeb2ede into main 2026-08-16 22:31:38 +00:00
art deleted branch issue-127-tile-weight 2026-08-16 22:31:38 +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!143
No description provided.