#127: tiles under the 0.5 MB budget — speckle floors, then a 1024-unit grid; heaviest tile 2.3 MB → 0.43 MB #143
No reviewers
Labels
No labels
blocked
claude-wip
map-report
map:is
map:se
needs-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
art/ismap!143
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-127-tile-weight"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
coverat z3–z7, and the weight was the coarse cascade'sinvisible 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
coverlayer(
zoom_filters, z3–z7): pieces smaller thanpixelSizeAtZoom(z)²— the samethreshold 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
extentunits across the tile and the client rescales that grid to howevermany 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
tileSizefor a vector source, whichhtml/style.jsondoes not override) — eight unitsper pixel — underneath geometry the cascade had already simplified with a tolerance of
pixelSizeAtZoom(z)/2, which,pixelSizeAtZoomcounting 256-pixel tiles, is one wholeCSS pixel. Three bits per coordinate describing wiggles the simplifier had already ruled
invisible.
mvtExtent = 1024is two units per CSS pixel, one per device pixel at 2×. Snappingto 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
coveropen.mvtBuffer = 64keepsthe 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'sextentis a separate argument fromST_AsMVTGeom's and both defaultto 4096. The first version of this set only
ST_AsMVTGeom's: every function ran, everytile decoded,
tiles --checkpassed, and the map drew at quarter scale in the top-leftcorner of each tile. Three things stop that now —
mvtAggCallcarries the extent atevery call site,
TestMvtExtentIsDeclaredEverywherepins both, andtiles --checkasserts the extent the tile declares (and that no feature arrives without a geometry,
which is what the new
WHERE q.geom IS NOT NULLfilters out: three sub-unit polygons inthe Norrland tile).
Before → after → after (
coverlayer,importer tiles --size)And the whole tile (
zxy_map, every layer), heaviest tile at each zoom from a fullscan of all 3 487 non-empty tiles in z3–z9:
Tiles over 512 kB: 7 → 0. z10–z15, 9×9 grids over the four capitals: ≤ 288 kB. All
layers other than
coverwere 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.
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.
(is 75 292 / 23 888, se 325 292 / 245 290, pl 379 918 / 227 181, fi 101 497 / 49 798).
conf/semap.yamlrecord 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 (
ad7f16fand part of61ceeb2):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.yamldisagreed 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_coverso the units cannot drift again.Also here, from #139's tool
Extending
tiles --checkmeant reading it: its docs named the pre-#129 flat flags(
-tile-size-top,-tile-size-zooms,-tile-bench-funcs) the subcommand rewriteremoved, 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, plusTestMvtExtentIsDeclaredEverywhere.importer tiles --checkfor every config (ismap/semap/plmap/fimap/world) — R121PASS, cover keys
[cover_class country]unchanged, extent and geometry assertionsgreen (and verified to FAIL when the extent is deliberately half-applied).
--acceptance all, incl. the 100 sR35 carve check), pl 29/29, fi 34/34, world 14/14.
--regeneralize 3-7(cover only), all fourcountries; no maxz re-import, no fetch, no destructive ops. Pass 2 changed no data —
it is generated SQL, reinstalled by
ensureParentSQLon 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.)
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 #127to #127: drop the coarse cover speckle — z5 tile 2.3 MB → 0.79 MB, z3–z7 all 52–66% lighterReview — 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
extentunits across the tile and theclient 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
tileSizefor a vector source, whichhtml/style.jsondoes not override): eight units per pixel, underneath geometry the cascade had already
simplified with a tolerance of
pixelSizeAtZoom(z)/2— which,pixelSizeAtZoomcounting 256-pixel tiles, is one whole CSS pixel. Three bits per coordinate describing
wiggles the simplifier had already ruled invisible.
mvtExtent = 1024is two units per CSS pixel, one per device pixel at 2×. Snapping toit 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
coveropen.mvtBuffer = 64keeps the bleed at the same1/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:
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'sextentis a separateargument from
ST_AsMVTGeom's and both default to 4096. The first version set onlyST_AsMVTGeom's — every function ran, every tile decoded,tiles --checkpassed, andthe map drew at quarter scale in the corner of each tile. Three things stop that now:
mvtAggCallcarries the extent at every call site,TestMvtExtentIsDeclaredEverywherepins both, and
tiles --checkasserts the extent the tile declares (see the #139 review).Review findings on the pass itself
The measurements recorded in
conf/semap.yamlcontradict 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_coverso the units cannot driftagain.
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_filtersblocks. Twenty hardcodedthresholds across four config files, and they are all just
pixelSizeAtZoom(z)²— anumber the importer already computes.
coveris a shared layer with a contract inconf/common.yaml; if the contract could carryzoom_filters, this would be one blockinstead 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 --checkclean 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-browserA/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
ensureParentSQLon the nextimport of any config. The Iceland-only layers (
contours,peaks,buildings,airports,airport_points) are per-country, so their functions regenerate only whenthose 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.
#127: drop the coarse cover speckle — z5 tile 2.3 MB → 0.79 MB, z3–z7 all 52–66% lighterto #127: tiles under the 0.5 MB budget — speckle floors, then a 1024-unit grid; heaviest tile 2.3 MB → 0.43 MB