makepad/map.md
Admin fc63b33a84 map: Europe tile conversion pipeline + offline GPS navigator
Conversion tooling (tools/map_tiles): curated Shortbread base pyramid
from a VersaTiles planet archive (bbox extract, brotli->gzip transcode
parallelized per 256x256 block), all-tag native OSM pbf detail
converter, pbf audit, and nav-build/nav-probe producing the routing
graph + search index artifacts. download_map.sh orchestrates pinned
downloads and conversions. mbtile_reader writer now skips SQLite's
lock-byte page at byte offset 1 GiB — allocating through it corrupted
every database over 1 GiB ("2nd reference to page 16385"); the 31 GB
Europe conversion passes integrity checks.

Navigation (libs/map_nav, new): region.search place/POI/street/address
index (prefix autocomplete, NL/EN category synonyms, proximity
ranking) and region.graph routing graph (CSR directed edges,
car/bike/foot speeds, oneway, turn restrictions, snap grid, A*),
maneuver generation and the NavSession map-matching state machine;
26 unit tests.

MapView interaction layer: MapViewAction, camera API + animated
fly_to, overlay.rs (route polyline with traveled dimming, markers,
position puck), per-widget mbtiles_path override, archive
minzoom/maxzoom honored for tile requests, zoom-level cross-fade over
the previous level's imagery, floating panels win hit-testing.
examples/map is the navigator app: worker-thread search, Drive/Bike/
Walk routing, simulated turn-by-turn with banner, follow camera.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 20:13:21 +02:00

36 KiB
Raw Permalink Blame History

Makepad map renderer repair plan

Date: 2026-07-27

Goal

Bring makepad-example-map to an OpenStreetMap Standard-like quality bar, especially when overzooming the local Shortbread tiles from source zoom 14 to view zooms 1517:

  • show the building footprints and other data that are already present in the MBTiles;
  • preserve polygon and line geometry instead of exposing source-resolution shortcuts;
  • keep road widths, joins, caps, dashes, and antialiasing well behaved at every view zoom;
  • use deterministic cartographic layer ordering;
  • remove cracks, spikes, overdraw fragments, and tile-boundary artifacts;
  • add enough diagnostics and fixtures that later style work cannot silently regress geometry.
  • retain the raw Simple 3D Buildings attributes needed for optional 2.5D extrusion.

The OSM.org screenshot is the visual reference, not a requirement to reproduce every OpenStreetMap Carto feature or pixel. Shortbread is intentionally a leaner schema, so it is the compact visual base rather than the completeness source. A second all-tag detail archive is required for benches, playgrounds, uncommon tags, and features outside Shortbread.

Current execution scope is conversion tooling and data validation only. Do not launch makepad-example-map; visual/runtime work below is the follow-up plan.

What the investigation found

1. The buildings are present; most are being hidden

The local source is not missing Amsterdam's building data. For the tile around the supplied street-level screenshot, the generated cache contains:

Tile z14/x8414/y5384 Ways/rings tagged with the layer
buildings 6,880
water_polygons 99
land 69
sites 50
all ways 10,581

The tile's feature runs arrive in this order:

water lines → piers → bridges → street polygons → streets → labels
→ buildings → water polygons → land → sites

The fill builder in tile.rs preserves encounter order and increments fill_zbias for every polygon. That paints water_polygons, land, and sites after buildings. Shortbread explicitly describes land as a base layer drawn first and sites as above land but below buildings. The current order is the reverse of that contract.

This explains the large pale blocks and isolated dark building fragments in the close-up: residential land/site polygons cover the building footprints, with only pieces left visible outside the covering polygon.

2. Fine geometry is deliberately discarded before tessellation

project_way_points_with_nodes converts each point to global f32 world coordinates and drops any point less than 0.5 source-tile units from the last retained point:

if dx * dx + dy * dy < 0.25 {
    continue;
}

That shortcut is almost invisible at source zoom 14, which agrees with the observation that the startup-level view looks acceptable. At view zoom 17 the same discarded detail is up to four logical pixels wide.

For cached tile z14/x8414/y5384, the current filter sees:

Segment result Count
retained 58,273
non-zero segments below 0.5 units and discarded 22,512
already collapsed to zero length 9,125
discarded non-zero building segments alone 17,975

This is a primary cause of clipped corners, diagonal shortcuts, malformed narrow buildings, and small notches becoming conspicuous when overzoomed.

3. Overzoom scales baked vector styling instead of only map positions

Shortbread's maximum tile zoom is intentionally 14; higher client zooms are expected to use overzoomed z14 data. The current renderer tessellates fills and strokes once at tile zoom and then applies:

map_scale = 2^(view_zoom - tile_zoom)

to the entire finished mesh. At view zoom 17 this is 8×, so it also magnifies:

  • road casing and center widths by 8×;
  • the fill and stroke antialias fringe from roughly one unit to roughly eight units;
  • butt-cap and join defects;
  • source-level clipping and simplification errors.

A production vector renderer scales centerline/polygon positions with the map, but evaluates style widths and antialias coverage in screen space using zoom-dependent style stops. This is why OSM's roads remain controlled at street level while the current roads become broad masks over surrounding detail.

4. Several Shortbread layers are dropped or interpreted as the wrong primitive

Shortbread layer/data Current behavior Required behavior
buildings filled, then commonly overpainted; no high-zoom outline above land/sites, with a thin outline from z15
land mapped to generic landuse/leisure base land pass, first
sites coerced into generic landuse/leisure dedicated pass above land and below buildings
street_polygons normalized as a highway, then explicitly excluded from strokes; never filled road/pedestrian area fill with correct bridge/tunnel order
bridges normalized as a highway, then explicitly excluded structure/casing and road pass above water/normal roads
dam_polygons, pier_polygons not normalized, therefore dropped structure fills
dam_lines, pier_lines treated as generic waterways structure strokes, not rivers
rail features in streets rail=true is ignored and the feature becomes a highway railway styling based on rail/kind
boundaries dropped zoom-dependent boundary strokes
place_labels, boundary_labels, water labels dropped point/line labels with hierarchy
addresses, most pois dropped house numbers/POIs at close zoom, subject to collision rules
aerialways, ferries, public transport broadly coerced to highways or dropped separate semantic styles

The style API currently supports only building/water/landuse/leisure fills plus a few line families, so the decoder is forced to invent OSM-like tags and loses the source schema's meaning.

5. The MVT path is needlessly lossy and obscures topology

Local tile handling currently performs:

MVT integer geometry
→ tile-local lon/lat
→ generated Overpass-shaped JSON rounded to 8 decimals
→ JSON parse
→ Web Mercator global coordinates
→ f32
→ generic WayData
→ reconstruct feature/ring identity from private string tags

This:

  • discards the typed MVT layer and geometry model too early;
  • creates a new node for every path point;
  • duplicates polygon closure points;
  • converts exact tile integers through two coordinate systems and text;
  • makes paint order depend on protobuf layer order;
  • forces polygon identity and ring order through __mp_* string tags;
  • makes it difficult to distinguish true endpoints from tile-clipped endpoints.

MVT already provides the exact tile-local integer coordinates, extent, geometry type, layer, feature id, ring order, tags, and per-layer feature order needed by the renderer.

6. Polygon and tile-edge handling needs hardening

The current ring grouping assumes that the first ring's winding establishes the exterior winding and that subsequent ring order is valid. That is normally valid for conforming MVT, but the data is first reconstructed through the JSON path and then globally quantized. There is also a silent 500-ring truncation named EARCUT_MAX_RINGS, although the active fill primitive is the sweep tessellator.

Fills are not clipped by the map code to a consistent tile-local buffered rectangle. MVT geometry is allowed to extend outside its tile as a render buffer, so neighboring tiles can paint overlapping, independently tessellated fragments. Artificial tile-cut edges also get the same magnified AA fringe as real polygon boundaries. This can create seams or wedges even after feature ordering is fixed.

7. Coordinate precision is a contributor, but should be isolated rather than assumed

The more strongly evidenced primary faults are paint order, point removal, semantic mapping, and scaled primitives. Precision is still measurable:

  • MVT extent 4096 provides a 1/16 tile-unit grid at z14.
  • Global z14 Amsterdam X coordinates are around 2.15 million, where f32 spacing is 0.25.
  • In a sample of the problem tile, converting projected positions to global f32 produces a median 0.5 and maximum 1.0 logical-pixel position error after 8× overzoom, before the GPU multiply/large-offset cancellation.

The fix is useful regardless of how visible this individual error is: keep geometry tile-local and compute only the small tile-to-viewport translation in f64 on the CPU. Validate this with an A/B capture rather than treating it as the sole explanation.

8. Overzoom also requests far too many source tiles

visible_tile_keys computes the center in request-zoom coordinates but subtracts the viewport's screen-pixel half-size without dividing by the overzoom scale. At view z17 with request z14, it considers an area eight times too wide and eight times too tall. This causes excess decoding, tessellation, GPU geometry, and cache growth. It does not directly deform shapes, but it will make the correct high-detail renderer unnecessarily expensive.

There is also a duplicate push in fill_ready_descendants, and local MBTiles batches scan an entire zoom level instead of querying only the requested (x, y) rows.

9. The raw Europe extract and the map-ready tile source are different artifacts

The current Geofabrik Europe extract is a 34,701,836,396-byte OSM PBF. It is the right archival/source dataset, but it is not directly usable by the map widget: producing a good z014 pyramid also requires Shortbread schema interpretation, zoom-dependent filtering, generalisation, clipping, and MVT encoding. Treating that pipeline as a mere container conversion would recreate the same data-interpretation bugs in a different tool.

Geofabrik currently publishes experimental Shortbread MBTiles for individual European regions, but not one current Europe-wide MBTiles file. VersaTiles publishes the same kind of ready-made Shortbread MVT pyramid for the full planet. The pinned 2026-06-08 archive is 66,534,652,244 bytes, covers z014, and stores Brotli-compressed tiles in an indexed .versatiles container.

The practical local pipeline has two independently tiled products:

Geofabrik europe-260726.osm.pbf
    canonical raw OSM source, retained and checksum-verified
        → native Rust multi-pass converter at source z14
        → compressed disk-backed node/way indexes
        → buffered integer clipping and external tile-block sorting
        → gzip MVT with original IDs, object types, and every source tag
        → indexed local/maps/europe-osm-detail.mbtiles

VersaTiles osm.20260608.versatiles
    official prebuilt Shortbread z014 pyramid
        → select the Europe bounding rectangle block-by-block
        → transcode selected Brotli MVT payloads to gzip MVT
        → stream them into local/maps/europe-shortbread.mbtiles

The base archive supplies inexpensive low-zoom cartography and established semantic layers. It must never be described as a complete conversion of raw OSM. The detail profile separately:

  • processes every tagged node and every way;
  • copies every source tag without a whitelist;
  • carries the original OSM feature ID and explicit node/way/relation type;
  • disables feature combining, feature limits, simplification, and low-zoom generalisation;
  • preserves every tagged way as a line and also writes closed ways as polygons, so conversion does not make an irreversible area interpretation;
  • accepts tagged relations and writes those for which spatial geometry can be assembled.
  • retains building:part, explicit/minimum height, level counts, roof geometry, materials, and colours verbatim for a later 2.5D mesh builder.

MVT cannot encode an untagged support node or a relation that has no standalone spatial geometry. The verified PBF therefore remains the canonical lossless artifact and is never deleted after conversion. “All data available” means every tagged spatial element is demand-streamable from detail tiles, while the PBF retains the complete original graph, including relation membership.

10. MBTiles can be both standard and efficient for this app

MBTiles 1.3 uses a SQLite metadata interface and a tiles interface with TMS-oriented Y rows. Its conventional (zoom_level, tile_column, tile_row) index is optional. The existing minimal reader did not use its parsed index and get_tiles_at_zoom scanned an entire zoom, which is not viable for Europe.

The new pure-Rust writer keeps the standard tables and metadata, but orders tile-table rowids deterministically by zoom, 256×256 block, local Y, and local X. A private metadata marker lets the Makepad reader compute the rowid and descend the table B-tree directly in O(log n). Other SQLite/MBTiles readers still see an ordinary, valid MBTiles database; legacy archives remain readable through the compatibility scan path.

The native raw-detail converter uses the same deterministic Makepad block rowids as the Shortbread extractor. The reader computes the rowid and descends the tiles-table B-tree directly. It also understands conventional third-party (zoom_level, tile_column, tile_row) indexes. Both layouts therefore provide direct O(log n) cold lookup; an index-free legacy archive alone uses the compatibility scan.

The writer is streaming:

  • it never holds the Europe tile payload set in memory;
  • it writes 64 KiB SQLite table, interior, and overflow pages directly;
  • it converts XYZ input to standard TMS rows;
  • it preserves gzip-compressed MVT blobs without decoding them again in the app;
  • it validates monotonic row order and refuses malformed coordinates;
  • release tests exercise overflow chains, multi-level B-trees, direct lookup, and a complete synthetic VersaTiles-to-MBTiles conversion.

The pinned real archive also passes a no-write planning scan. The Europe rectangle selects 483 of 4,853 blocks, 24,564,277 tiles, and 24.39 GiB of Brotli-compressed source payload. This scan reads only the container indexes; it does not create an output file or load tile payloads into memory. Gzip MBTiles output will be larger than that Brotli payload total, and the all-tag detail build needs substantially more temporary SSD space.

11. The all-tag compiler is now native Rust and restartable at the expensive boundary

Tilemaker 3.1.0 source was pinned at commit e16203e4e2fb38a11580621fc0503ef463ab849f and inspected only as an architectural reference; it was not built or run and is not a runtime/build dependency. The replacement Rust path performs five bounded-memory stages:

relations → bounded relation-way membership bitset, then persist it
nodes     → one-time projection + compressed coordinate chunks + direct group index
ways      → direct node resolution + compressed relation-way references
relations → member stitching + multipolygon outer/hole assignment
spools    → external block sort → gzip MVT → streaming pure-Rust MBTiles

The PBF advertises Sort.Type_then_ID, which makes these sequential passes possible without a continent-sized in-memory object graph. Geometry is projected once to integer z14 MVT coordinates, exact consecutive duplicates alone are removed, and line/polygon clipping uses a consistent 64-unit buffer. Tile records are spooled per 256×256 block with only 32 open files and externally sorted with a configurable memory ceiling.

Every original tag stays a string. This matters for 2.5D data because OSM heights can carry units and level/roof attributes are not interchangeable. The output metadata names the supported 2.5D keys, while the renderer can normalize them once during mesh preparation:

building, building:part
height, min_height
building:levels, building:min_level
roof:shape, roof:height, roof:levels
roof:direction, roof:orientation, roof:angle
building:material, building:colour
roof:material, roof:colour

After passes 14, an atomic spool.complete.json marker and source audit are written. If the final MBTiles pass is interrupted, rerunning with the same source/store validates the source size and zoom and restarts only external sorting/MBTiles writing. Incomplete earlier stores are refused rather than guessed at.

A real Monaco Geofabrik smoke conversion produced 109 z14 tiles from 41,524 nodes, 6,209 ways, and 348 relations. SQLite quick_check passed, a cold direct lookup succeeded, all six MVT layers decoded, and the largest tile contained actual height, building-level, building-part, roof, material, and colour keys. Re-running the final pass from completed scratch produced a byte-identical MBTiles file.

The completed Europe source audit contains 3,792,033,607 nodes, 464,000,109 ways, 4,793,955,052 way-node references, and 1,840,402,362 tags. It specifically finds:

Europe PBF feature tag Count
amenity=bench 2,696,309
leisure=playground 618,114
building 251,162,480
building:part 3,126,026
height 5,457,826
min_height 177,798
building:levels 25,386,641
building:min_level 139,202
roof:shape 7,431,109
roof:height 349,899
roof:levels 4,559,236

The optimized release converter was then benchmarked on the 47,228,875-byte Luxembourg extract: 4,208,128 nodes, 6,313,908 way-node references, and 1,617,855 tile records became 3,953 validated tiles in 12.3 seconds. Pass 3 sustained about 1.29 million resolved references/second. Scaling each measured pass by the audited Europe counts gives a hardware-specific preflight estimate of roughly 23 hours, with output remaining byte-identical before and after the optimization pass.

12. Why the OpenStreetMap.org reference looks substantially richer

The Standard layer on openstreetmap.org is OpenStreetMap Carto, a Mapnik raster rendering stack over a PostgreSQL/PostGIS database imported and preprocessed by osm2pgsql. Its project layers contain explicit SQL feature selection, CartoCSS styling, and deliberate render order. Buildings have a dedicated source/style and gain an outline at close zoom; shops, roads, tunnels, landcover, and labels occupy intentionally separate passes.

That is materially different from the current Makepad path:

OSM Standard:
raw OSM → rendering-specific PostGIS schema → per-layer SQL/style → Mapnik raster tile

current Makepad:
Shortbread z14 MVT → generic OSM-like tag coercion → encounter-order meshes
                    → scale the complete z14 primitive when overzooming

So the reference's house coverage is not explained by precision or palette. It comes from retaining the building source, giving it a dedicated paint pass, applying zoom-specific styling, and rendering primitives for the requested display zoom. The local Shortbread tiles already contain thousands of those footprints; the renderer presently hides or deforms many of them.

Implementation plan

Dataset and container pipeline — utilities ready; large conversions not started

  • Pin the Geofabrik Europe PBF and VersaTiles snapshot instead of downloading mutable latest URLs.
  • Add resumable downloads, publisher checksum verification, partial filenames, and atomic final renames to download_map.sh.
  • Download and MD5-verify local/maps/europe-260726.osm.pbf (34,701,836,396 bytes).
  • Download and SHA-256-verify local/maps/osm.20260608.versatiles (66,534,652,244 bytes).
  • Extend makepad-mbtile-reader with a pure-Rust streaming SQLite/MBTiles writer and deterministic direct tile lookup.
  • Add makepad-map-tiles, a Rust extractor that reads VersaTiles v02 block/tile indexes, selects Europe through z14, transcodes Brotli MVT to standards-compliant gzip PBF, and writes MBTiles.
  • Implement the all-tag raw-PBF compiler entirely in Rust. It retains IDs/types/all tags, emits both line and polygon interpretations for closed ways, assembles spatial relations, clips in integer tile coordinates, externally sorts bounded tile-block spools, and performs no geometry simplification.
  • Add a parallel streaming PBF audit command and an indexed-MBTiles direct probe command to makepad-map-tiles; neither loads the Europe dataset or tile table into memory.
  • Audit the 2.5D building family explicitly while copying all source tags verbatim: heights/min-heights, levels/min-levels, building parts, roof geometry, materials, and colours.
  • Extend makepad-mbtile-reader to perform direct lookup through standard third-party MBTiles composite indexes as well as Makepad's deterministic rowids.
  • Change the local map loader to use direct requested-tile lookups for Makepad-authored files while retaining the legacy full-zoom compatibility path.
  • Treat MBTiles itself as the local disk cache: do not duplicate local tiles into millions of generated JSON files, sort visible requests into on-disk block order, and keep B-tree page reuse in a bounded 32-page cache.
  • Add and run a no-write planning scan against the real tiled source; it validates the v02 indexes and reports the exact Europe selection before committing disk space.
  • Smoke-convert 23 real Amsterdam tiles, pass PRAGMA quick_check, and directly fetch gzip tile 14/8414/5384 through the Makepad rowid path.
  • Convert a real raw Monaco PBF through every native pass; validate 109 gzip-MVT tiles, all six detail layers, direct lookup, actual 2.5D tag keys, dynamic source bounds, and a byte-identical final-pass restart.
  • Produce local/maps/europe-shortbread.mbtiles, run SQLite integrity/schema checks, and verify representative Amsterdam z14 tiles and layer counts.
  • Produce local/maps/europe-osm-detail.mbtiles on a volume with sufficient SSD workspace; verify the tile index, metadata, and representative amenity=bench and leisure=playground features against the source PBF.
  • Add a PBF-versus-detail audit report: tagged node/way/spatial-relation totals, feature IDs, tag-key/value samples, and explicit counts for benches, playgrounds, addresses, and buildings. A mismatch must fail conversion validation rather than be treated as styling.
  • UI launch and screenshot comparison are deliberately deferred; the current task stops at converter/database validation.
  • If a fully native curated Shortbread pyramid compiler is later desired, scope its zoom-dependent schema mapping and generalisation as a separate product. The completed native all-tag detail compiler intentionally avoids those irreversible cartographic decisions.

Prepared commands

Run these independently from the repository root:

./download_map.sh raw       # checksum-pinned 34.7 GB Europe OSM PBF
./download_map.sh tiles     # checksum-pinned 66.5 GB planet Shortbread source
./download_map.sh plan      # validate indexes and estimate Europe selection; writes nothing
./download_map.sh audit     # bounded-memory source feature/tag manifest
./download_map.sh convert   # streaming Europe Shortbread MBTiles build

# Put the bounded native scratch store on a spacious fast SSD.
DETAIL_STORE=/external/ssd/native-detail-europe.store \
DETAIL_SORT_MEMORY_MIB=256 \
./download_map.sh detail

./download_map.sh verify

convert and detail write to distinct .partial.mbtiles files, validate before atomic rename, and refuse to overwrite stale partial results. The raw PBF is retained after both.

Phase 0 — Lock down evidence and visual regressions

  • Add a small raw-MVT fixture and synthetic fixtures for:
    • a polygon with a hole;
    • a multipolygon with two exteriors and holes;
    • rings touching/continuing through a tile buffer;
    • short building edges below 0.5 tile units;
    • road junctions, dead ends, bridges, and tunnels.
  • Add decoder assertions for layer name, geometry type, extent, feature id, original feature order, tags, ring count, and point count.
  • Add a diagnostic layer counter. A valid feature must be rendered, intentionally filtered by a named style rule, or counted as rejected with a reason.
  • Add temporary debug toggles for one-layer-at-a-time rendering, tile boundaries, ring winding, and primitive wireframes.
  • Define fixed Amsterdam camera bookmarks at z14, z15, z16, and z17, including the Bloemgracht/Rozengracht area from the screenshots.
  • Capture baselines at device scales 1× and 2× through the release Studio RunItem flow.

Phase 1 — Restore the data already present

  • Introduce an explicit semantic paint order independent of MVT protobuf order and HashMap iteration. A starting order is:

    background/ocean
    → base land
    → water polygons
    → sites/landuse overlays
    → buildings
    → dams/piers/street areas
    → tunnels
    → ordinary waterways/rail/roads
    → bridges and bridge roads
    → boundaries
    → labels/icons
    
  • Preserve the source feature index as a stable secondary sort key. Shortbread documents some layers, especially streets, as already sorted for rendering.

  • Render buildings after land and sites; add a restrained building outline at z15+.

  • Give sites, street_polygons, piers, dams, and bridges explicit fill/stroke rules.

  • Interpret streets.rail and kind before synthesizing a highway classification.

  • Keep bridge, tunnel, link, and source z-order attributes through all batching stages.

  • Stop grouping ordered strokes through an unordered HashMap unless the grouping key also retains the complete paint order.

This phase should immediately reveal the thousands of building footprints already in the local tile and remove the largest apparent “missing data” problem.

Phase 2 — Replace the MVT-to-JSON round trip

  • Decode local PBF directly into a typed model, for example:

    DecodedTile {
        layers: Vec<DecodedLayer {
            name,
            extent,
            features: Vec<DecodedFeature {
                id,
                source_order,
                geometry_type,
                tags,
                paths: Vec<Vec<IVec2>>,
            }>,
        }>,
    }
    
  • Keep MVT coordinates as integers until geometry preparation; convert once to tile-local f32 coordinates such as x * 256 / extent.

  • Keep the network/Overpass source as a separate adapter that emits the same typed intermediate model rather than pretending local MVT is Overpass JSON.

  • Cache raw source payloads or decoded typed data, not generated 8-decimal JSON.

  • Version or remove local/tilecache_v4 when this lands so stale lossy geometry cannot mask the fix.

Phase 3 — Make topology and clipping exact

  • Remove the < 0.5 radial point filter. Initially remove only exact consecutive duplicates in integer tile coordinates.
  • If simplification is needed later, use a topology-safe, view/style-aware tolerance and never simplify building corners merely because they were subpixel at source z14.
  • Group MVT polygon rings directly according to the MVT rule: positive tile-coordinate area starts an exterior, following negative-area rings are its holes.
  • Validate and report malformed rings rather than silently losing them.
  • Replace the 500-ring truncation with a documented budget/fallback that never turns a valid multipolygon into a different shape.
  • Do all clipping in tile-local coordinates with a consistent render buffer.
  • Distinguish true line endpoints from clipped endpoints so real dead ends can use the intended cap while tile cuts remain seamless.
  • Prevent AA on artificial internal tile cuts, or overlap/scissor buffered fill bodies in a way that guarantees no transparent seam between neighboring tiles.
  • Add cross-tile tests asserting that the same source edge meets at exactly the same screen coordinate during fractional zoom and pan.

Phase 4 — Use zoom-correct vector primitives

  • Separate position scaling from style scaling. Overzoom map coordinates by 2^dz, but evaluate stroke width from the current view zoom.
  • Add zoom stops/interpolation to road, rail, waterway, casing, outline, and label styles. Do not obtain a z17 road by multiplying a z14 baked width by eight.
  • Keep fill and stroke AA approximately one device pixel at every view zoom and DPI.
  • Prefer GPU-side screen-space extrusion for line width/AA, or cache tessellation by a small style-zoom bucket if the vertex format cannot yet represent centerline plus extrusion.
  • Use the existing GPU-expandable fill approach, or an equivalent map-specific variant, so fill fringes do not grow with map_scale.
  • Rework road passes as deterministic casing/center layers with correct round joins, true-end caps, intersections, bridge decks, and tunnel dashes.
  • Preserve a stable line metric across features/tiles for dash phase.
  • Remove the current center-overlay width workaround once the proper stroke primitive makes seams unnecessary.

Phase 5 — Reach the OSM-like cartographic quality bar

  • Use the official Shortbread styles (especially VersaTiles Neutrino/Colorful) as the semantic source-layer and zoom-stop reference; use OpenStreetMap Carto as the visual hierarchy/palette reference.
  • Cover every Shortbread layer intentionally. Keep a checked-in coverage table with rendered, label-only, or deliberately omitted for each layer.
  • Tune the light theme first:
    • visible, separately outlined buildings;
    • distinct but quiet residential/commercial/industrial land;
    • water without heavy magnified borders;
    • clear road class hierarchy;
    • correctly layered bridges, tunnels, rail, paths, piers, and pedestrian areas;
    • parks and sites that do not cover buildings.
  • Then derive the dark theme from the same semantic order and zoom stops.
  • Extend labels in this order: street labels, place hierarchy, water names, boundary labels, house numbers at close zoom, then POIs/icons.
  • Add road shields and multiline/ref handling only after base collision behavior is stable.
  • Keep label size in screen pixels and make repeat/collision distances zoom-aware.

Phase 6 — Recover performance after correctness

  • Fix visible-source-tile bounds by dividing viewport size by 2^(view_zoom - request_zoom).
  • Query requested MBTiles rows directly for both Makepad-authored files and conventional indexed MBTiles instead of reading all tiles at a zoom for every batch.
  • Remove the duplicate descendant push.
  • Separate decoded-tile caching from style/geometry caching so theme changes do not redecode PBF.
  • Keep one reader/file handle alive in the tile I/O worker across batches; retain a strictly bounded page cache and cancel queued batches made stale by a newer camera epoch.
  • Key prepared geometry by tile, style epoch, and only the zoom bucket actually needed.
  • Measure decode, topology preparation, tessellation, upload, draw, and label placement independently in release builds.
  • Establish memory and frame-time budgets after the correct building count is visible; do not regain speed by silently dropping geometry.

Acceptance criteria

Data and ordering

  • The Shortbread base and all-tag detail archive have distinct metadata/source-kind markers; no validation may infer raw-OSM completeness from Shortbread.
  • Every tagged spatial feature selected from the PBF retains its original OSM ID, type, and complete tag map in the detail archive. Benches and playgrounds are explicit audit fixtures.
  • Building footprints/parts retain explicit height and minimum-height strings, level fallbacks, roof geometry, materials, and colours; the renderer can extrude them without a second database or lossy conversion.
  • Closed ways remain available as source lines and polygon candidates; conversion does not silently discard either interpretation.
  • Every valid buildings polygon in the fixture tile is emitted or has a logged rejection reason; land/site passes cannot cover the building pass.
  • street_polygons, bridges, piers/dams, rail, and boundaries appear in their documented semantic order.
  • The renderer's output is deterministic across runs regardless of hash seed or worker order.

Geometry

  • Short building edges and corners remain present from z14 through z17.
  • Holes and multipolygon islands render correctly.
  • No spikes, diagonal shortcuts, transparent cracks, or doubled fringes appear at tile boundaries.
  • Continuous zoom and pan do not make shared edges separate or jump.
  • A precision A/B confirms whether tile-local coordinates produce a visible improvement; the architecture keeps them local either way.

Primitives and style

  • Road widths follow explicit zoom stops and do not grow by 2^dz.
  • Fill and line AA remains about one device pixel at 1× and 2× DPI.
  • Junctions have continuous casings/centers; true endpoints and clipped endpoints have the correct caps.
  • Buildings are individually legible at z1517 with a subtle outline comparable in hierarchy to the OSM.org reference.

Runtime

  • At view z17/source z14, only intersecting z14 tiles plus the chosen prefetch ring are requested.
  • Opening the Europe archive must not enumerate its tiles. A cold visible batch reads only the metadata B-tree, the requested tile B-tree paths, and those tiles' payload/overflow pages; steady-state caches remain bounded independently of archive size.
  • Conversion validation uses release CLI tests, publisher checksums, SQLite checks, direct tile probes, and PBF-versus-MVT audits; it does not launch the example app.
  • The corrected dense Amsterdam view remains interactive in release mode without geometry loss as a performance shortcut.
  1. Diagnostics, fixture counts, and fixed camera screenshots.
  2. Explicit paint order and source-layer coverage; confirm that buildings reappear.
  3. Direct typed MVT decode; delete the JSON round trip and < 0.5 point filter.
  4. Tile-local topology, clipping, and cross-tile seam tests.
  5. Screen-space line/fill primitives and zoom-dependent style stops.
  6. Complete Shortbread styling and label hierarchy.
  7. Tile selection, MBTiles query, cache, and profiling cleanup.

Do not start by palette-tuning the current meshes. Correct data interpretation, topology, and screen-space primitive behavior first; otherwise style changes will merely hide defects that reappear at another zoom or DPI.

References