Tile Geometry and Topology - krcenov/RNS510-Map-Viewer GitHub Wiki
Tile Geometry and Topology
Status: π‘ Partially cracked. Road geometry (the actual polylines) is
β
cracked and validated at scale. Point-to-point connectivity
(topology/adjacency) is π‘ cracked for most features, with real,
characterized failure modes. This page covers what's inside a decompressed
mp0/mg1βmg4 tile β see MAP_COMPRESSED Container Format
for how to get a decompressed tile in the first place.
Reader: research/map_compressed_reader.py.
Tile header
0x0000: magic0x0146(constant across every tile sampled)0x0002β0x0017: small header fields, uint16 values never exceed ~2000 β likely local tile-relative extents/counts, not decoded.0x0018onward: a spatial sub-index of individual uint16 LE words (each eitherdeclen, used as an "empty cell" sentinel, or0, a second pad marker). Ends at a variable but heavily-clustered position: byte 326 for ~83% ofmg2/mg3/mg4tiles, byte 24 for sparse/degenerate tiles.mp0's sub-index is deeper/differently shaped.- Some header fields are now separately decoded via
decode_tile_header()(e.g. an exact "this feature's own topology-graph node count" field) β see the Topology section below.
Coordinate geometry β β cracked, validated at scale
The coordinate region is a sequence of self-delimiting feature blocks packed back-to-back with no padding:
+0 uint16 LE point_count (N) -- deltas in THIS block only
+2 int32 LE anchor longitude (degrees = value / 100000)
+6 int32 LE anchor latitude (degrees = value / 100000)
+10 N * (int16 LE dx, int16 LE dy) -- cumulative from the anchor
Block size = 10 + 4*N bytes; the next feature's block starts exactly
there. Blocks are read until a candidate anchor fails a geographic
plausibility check β that's where the coordinate region ends and the
topology table begins.
Validated at scale: exact-integer matching every decoded vertex of
every decoded feature (across the 80 largest mg4 tiles) against
eeu.rd's own int32 coordinate fields β same rigor as a bit-exact match,
no float rounding β found tiles with 2+ features each independently
landing an exact hit on a different real named road (i.e. genuinely
separate polylines, not one chain that happens to pass near several
roads). Cleanest example: a tile decodes to exactly 2 features, a
392-point chain matching 10 distinct named roads near IaΘi, Romania, and
an independent 154-point chain matching 2 more. Also confirmed on mp0
(one tile near Geneva exact-matches 24 named streets).
Known decode caveats
- Trailing "phantom" blocks: the greedy plausibility stop can
occasionally accept a few small (0β6 point) trailing blocks with no
corresponding
eeu.rdrecord before it finally hits real topology-table bytes and stops. Doesn't affect the large, validated features, but means "number of blocks" isn't a fully trustworthy feature count on its own. - Oscillating overrun (
trim_oscillation, opt-in, default off): some tiles contain a still-unidentified fixed-stride structure partway through a declared block that isn't real delta data β decoded naively, it produces huge (24β33km) single-step jumps oscillating between two fixed latitude bands. A detector (_find_oscillation_clusters()) finds and splits out these clusters, but tile-local statistics alone cannot reliably tell a real corruption cluster apart from a legitimate long road doubling back on itself β this was proven, not just suspected, with concrete counter-examples in both directions. The map viewer therefore defaults this filter off: turning it on was directly shown to delete real named roads (including Sofia's own ring road) in dense urban areas, a worse outcome than leaving the rare corruption visible as a few stray points.
Topology / adjacency table
Past the coordinate region, each feature (not each tile) gets its own
table of exactly point_count + 1 fixed 10-byte records (record 0 is a
non-node "header" record):
+0 2-byte tag -- low byte selects how many of the 4 fields are populated
+2,4,6,8 up to 4 x uint16 LE fields (A, B, C, D)
Finding the table β π‘ mostly solved
The table's start position isn't at a fixed offset from the feature's own
block_end β it's located by searching forward from the end of the
tile's whole coordinate region for a run of point_count + 1 records
whose fields all stay below a plausibility cap (record 0 exempted, since
it's a distinct "header" record with much larger values by design). On a
multi-feature tile, features are resolved tightest-cap-first, each
one's found byte range reserved against every other feature's search
β this stops a looser search from aliasing onto an already-claimed
table.
At-scale characterization (a real ~11kmΓ11km box around Sofia,
Bulgaria, 257 features across all 5 layers): 194/257 (75.5%) resolve
at high confidence, 1 at low confidence, 62 (24.1%) not found at all
β concentrated in dense street-level mp0 tiles (54/63 of the non-high
cases), not correlated with feature size. A "found": False result means
exactly that: no table could be located for that specific feature in this
tile, honestly reported rather than fabricated.
Node-id β coordinate mapping β β cracked (the "shared link-id" mechanism)
A record does not reference a neighbor's point index directly.
Instead, two really-adjacent points' records share one common field
value (a per-edge "link id"); which table record belongs to which point
index is offset by a small per-feature shift that must be discovered
per feature (not a fixed constant). resolve_topology_adjacency() scores
candidate shifts by the median real-world distance of the edges they
imply β the correct shift produces short, road-like edges; a wrong one
produces long, implausible ones.
Validated against two independent human-verified ground-truth tiles: 16/16 and 16/18 real, human-confirmed edges recovered exactly (the 2 misses on the second tile both touch one point, most likely a near-duplicate junction vertex, not a mechanism failure). A broader 75.5%-high-confidence characterization (above) followed from fixing two real bugs in the table-location search this uncovered.
Per-edge false-positive filter
Even within a feature whose overall confidence is "high", a small number
of individual edges can be wrong: two points sharing the literal link-id
value 0 get wrongly wired together, because 0 is almost certainly
a padding/unset-field sentinel (the same role plain 0 plays elsewhere in
this tile format), not a genuine edge id β so any two points that both
happen to have an unused field slot land in the same bucket and get
spuriously connected. Found via 5 real user-reported examples (two
"connected" points 692mβ2,917m apart). Fixed with an independent per-edge
distance sanity filter (max_edge_m, default 200m) applied after shift
selection β re-validated to leave both ground-truth ties unchanged while
dropping the 5 reported false edges and a number of other implausible
ones.
A predicted-but-unobserved limitation of this same filter CONFIRMED
and FIXED, a later session. The 200m default was calibrated entirely
from mg2/mp0 ground truth (dense layers, every real edge under
110m) and applied uniformly to every layer β never validated against
mg4/mg3, the coarsest layers, where 200m-2,000m between shape
points is normal. User-reported: a real mg4 edge (points 72β74,
1,518m, a genuine divided-road carriageway transition) was being
silently dropped. Auditing just one feature found 28 real edges
(200m-1,810m) dropped this way. Fixed at the caller level (not the
function's own default, which stays correct for the layers it was
validated against): the map viewer's
MAX_EDGE_M_BY_LAYER now passes 5,000m for mg4/mg3, unchanged
200m for mg2/mg1/mp0. Verified with a real redraw test: rendered
line pixels increased at 2 zoom levels (667β1,183, 1,055β1,460),
confirmed more real edges now drawn, not a regression.
Interleaved-parallel-chains as a divided-road detector β tested and
rejected, same session. The (72, 74) fix above is a genuine 2-chain
interleaved structure. Tested whether this generalizes into a usable
signal (scan for alternating gap=2 edges β flag divided roads), against
the same 4 human-confirmed ground-truth sites used for the seg_list
work (A1 x2, Hemus, A6). A wide-window check looked promising but was a
window-choice artifact; re-checked against the exact reported points'
own real neighbors instead: only 1 of 3 divided sites (Hemus) shows the
interleaved signature right at its reported location, the other 2 look
mostly sequential, and the non-divided control isn't clean either. Real
but inconsistent β not pursued as a detector.
Link-id value meaning β characterized at scale, still not fully explained
A direct follow-up to "what do the shared numeric link-id values
themselves mean?" Sampled 402 clean single-feature high-confidence
tiles (β₯30 points, all 3 layers, Bulgaria-wide) and classified every
non-zero value by how many points share it (123,618 (tile, value)
pairs): 74.2% shared by exactly 2 points (a clean edge β validates
the mechanism at far larger scale than the original hand-verified
ground truth), 25.4% are "orphans" (present in only 1 point's own
record, no local partner β meaning unresolved: unused padding? a link
stub that doesn't resolve locally? something else?), and 0.3% form
real junction groups (3+). Also found a harmless SECOND sentinel
pattern (values near the uint16 ceiling, >65,000, distinct from the
already-known 0 padding value) that skews naive range stats but was
confirmed to never form a spurious edge (0/597 tiles checked) β no code
change needed. Clean tiles' value ranges are dense ([1, K], no gaps),
consistent with a per-tile-authored LOCAL numbering, not an obviously
external key. Whether the values (or the orphans specifically) map to
anything in eeu.rd's own record layout was the natural next step β
tested immediately after, same session, and REFUTED: built a full
in-memory exact-coordinate index over all 8,809,081 eeu.rd records
(~50s) and compared each matched point's eeu.rd bytes 16-20/20-24
(uint32 LE β confirmed to hold small plausible-looking integers on
inspection, e.g. 67/43, 325/370) against its link-id value set. Across
2,963 qualifying points: only 20 matches (0.67%) β actually below
the ~1.6% naive chance-collision rate for value sets this size. Not
the external reference. Also tested eeu.rd's other 2 documented
"unresolved" fields the same session: bytes[5:8] (a plausible
shared-geometry pointer, noted elsewhere as "often shared across
same-name records") holds values around 3.6-7.7 million β far outside
any plausible link-id magnitude, ruled out on scale alone; bytes[1:5]
(the already-ruled-out road-class candidate) also scored 0/2,963. This
is now a complete sweep of every eeu.rd field documented as
unresolved or candidate β none of them are the link-id's external
reference. Its independent meaning remains genuinely open. See
eeu.rd for the record layout. Full writeup: resolve_topology_adjacency()'s own
docstring in research/map_compressed_reader.py.
Known remaining gaps
- Cross-feature edges: the obvious mechanism was tested and refuted,
same session.
resolve_topology_adjacency()resolves adjacency entirely within one feature. A real, confirmed example exists: a user-reported connectivity chain crossed from one feature's point into a different feature in the same tile β even though both features individually resolve at high confidence, the resolver has never tried bridging between them. There was a structural hint this should be possible (node-ids referenced in one feature's table have been observed going as high as the tile's combined point total across all features, suggesting a tile-global, not per-feature, id space), which raised the natural hypothesis: do two geographically-close dead-end points in different features share a link-id VALUE, the same mechanism used within one feature? Tested at scale (Bulgaria-wide, 3 layers, 646 qualifying multi-feature tiles): the near-vs-far shared-value hit rate is NOT elevated for nearby pairs in any layer (mg4 0.33% near vs 0.58% far; mg2 0.30% vs 0.33%; mp0 0.54% vs 0.68%) β if anything slightly lower β and many "hits" pair points tens of kilometers apart. This rules out cross-feature value-sharing as pure numeric coincidence, not a real mechanism. Cross-feature adjacency remains genuinely unsolved; this specific approach is closed, not just untried. Full writeup:resolve_topology_adjacency()'s own docstring inresearch/map_compressed_reader.py. - An even bigger version of the same gap: CROSS-TILE edges, real
example confirmed. User right-clicked 2 real
mg4points expected to connect: tile 2384 point 132 (23.45508, 42.63235) and tile 2385 point 12 (23.45595, 42.63224) β different tiles, ~72m apart. Confirmed the structural signature matches a genuine tile-boundary split exactly: both resolve "high" confidence locally, but each is a degree-1 dead end within its own tile (no further neighbor). The 2 points' own raw topology records share NO common field value β the intra-tile shared-link-id mechanism genuinely can't reach across tiles. This is the same problem the firmware string-mining found a real, NAMED pipeline for (db_vid_get_map_id_V000/db_vid_get_pcl_id_V000) but never cracked at the byte level β this example is real ground truth preserved for that future attempt. decode_topology()'s own graph-walk hypothesis was tested and definitively refuted: a single feature's topology sub-graph is provably not graph-isomorphic to its own point-sequence path (on a reference tile, it has two connected components, one containing a cycle) β so recoveringdecode_features()'s point order from a walk of this graph isn't possible even in principle for that tile.- The tag's field-count-vs-real-junction correlation is a real but weak
signal (found via
eeu.rdname-change cross-referencing at intersections) β not strong enough to use as ground truth on its own.
Practical relevance for editing
The geometry format is fully readable/writable per-feature, and a valid topology entry for a newly-inserted road's own vertices can now be synthesized in principle. What (if anything) real hardware actually needs from the topology table to render/route a new road correctly is untested on real hardware β see the main README's "Open problems" section.
Reader code
research/map_compressed_reader.py β decode_features(),
decode_tile_header(), decode_topology(), resolve_topology_adjacency()
(see that function's own docstring for the full mechanism writeup, every
validation number, and all known caveats in detail).