Map Viewer Tool - krcenov/RNS510-Map-Viewer GitHub Wiki

Map Viewer Tool

rns510_map_viewer.py is a read-only Tkinter GUI that renders the real map disc directly, built specifically to validate and debug the file format findings documented elsewhere in this wiki β€” not a production navigation UI.

Run with py rns510_map_viewer.py.

What it does

  • Pools all 5 rendering layers (eeuz.mp0, .mg1–.mg4) together, with per-layer visibility checkboxes.

  • Renders every decoded point as a dot, with an optional "Draw connected roads" mode built on the topology/adjacency resolver.

  • Click-to-identify: right-click any rendered point or connected-road edge to see exactly which layer/tile/feature/point it is β€” the tool used to find and report several real bugs in the topology resolver (ground-truth connectivity data, false-edge reports). A point pick also now shows "nearby streets" β€” the real street names from the nearest eeu.iof "anchor" record's own eeu.il-based list, if one exists within ~3.3km of the click. A real POI icon is checked first of all β€” a click on one shows its real name and category in the status bar.

  • A simulated Address Entry screen (Country β†’ City β†’ Street, live per-letter narrowing) built on eeu.ctr/eeu.cty/ the road name-search catalog, modeled on the real unit's own on-screen keyboard.

  • Discrete, real-hardware-style zoom levels (500km down to 25m) and bezel/hardware-button chrome matching the physical unit's layout.

  • Real POIs from EDB/POI/POI.DB3 β€” 4.73M real points of interest, gas stations to airports, unlocked by cracking the database's own Coordinate field. Zoom-gated by each category's own real, disc-designed MapZoomLevel (airports visible from far away, "downtown area" markers only up close), toggleable with a "Show POIs" checkbox, and drawn with the disc's own real category icons (gas pump, airplane, pharmacy cross, ...) β€” not plain dots. The same unified search box that already finds roads/cities also searches real POI names (tagged [POI], showing the real category).

  • Real Sirius TravelLink POIs (gas stations, from a separate NAR firmware disc's own SIRIUS.DB3), with real per-channel icons decoded from the .DDB channel-art cache β€” see Sirius TravelLink and North America Discs for the full format crack this feature is built on.

  • Live firmware screen-shape reading: File > Open Firmware ISO (real screen shape)... reads a real firmware ISO's own FHDD6.FLI and reshapes the canvas to the unit's own real aspect ratio (letterboxed/pillarboxed) β€” nothing hardcoded. See "v40 β†’ v41" below.

Architecture

MapData (all data/decode logic, no GUI β€” directly exercised by test_map_viewer.py) + App (the Tkinter GUI shell). Rendering is a single Pillow-rasterized bitmap per redraw (PIL.Image/ImageDraw, shown via one canvas.create_image()), not one Tkinter canvas item per point/edge β€” the earlier per-item approach didn't scale past a few thousand points.

Full changelog

This tool has gone through many iterations (v1 β†’ v42, as of this page), each one documented with real before/after numbers, root-caused bugs, and validation evidence. README.md's own Β§10 "Map viewer (visualization/validation tool)" section is the original, even more detailed source this page condenses.

("v22 β†’ v23"): POI search β€” completes the POI feature arc (crack β†’ display β†’ icons β†’ click-to-identify β†’ search). The same unified search box that already merges road + city results now also searches real POI names, via a new poi_db_reader.search_pois() (a plain case-insensitive linear scan over a precomputed uppercase name list, ~0.20s per search β€” fine for an explicit "press Enter" search that already runs in a background task). SearchHit gained a "poi" kind and a proper breadcrumb branch (previously any non-city hit fell through to a hardcoded "ROAD" label, which would have mislabeled a POI). Verified end to end with a real running-app test: searching "MCDONALD" returns real POI hits alongside road/city results; selecting one and jumping lands exactly on its real coordinates, with the breadcrumb correctly reading RESTAURANTS | POI | MCDONALD'S.

Also this session ("v21 β†’ v22"): POI click-to-identify β€” the natural next gap once real icons landed ("v20 β†’ v21"): clicking one did nothing. A real POI icon is now checked FIRST in the click-to-identify flow (wider pick radius than a road dot, matching a real 34Γ—39 icon's on-screen size), searching the EXACT set the last redraw actually drew after decluttering β€” so a click can only ever hit something visible β€” and shows the real name/category in the status bar. Verified with real running-app tests: a click on a real rendered POI reports its name/category correctly (including "(unnamed)" for the many real POIs with no Name), and a click in genuinely empty space correctly falls through with no false positive.

Also this session ("v20 β†’ v21"): real POI category icons, not plain dots. POI.DB3's own icon schema β€” PoiPartition.Icon_ID β†’ Image_BaseAttributes β†’ Image_ImageBlob_Relation β†’ ImageBlob.ImageData β€” turned out to be real, standard 34Γ—39 PNG files the whole way through (confirmed via PNG magic bytes), with every category name matching its own linked image's real name ("gas station" β†’ an image named "gas station"). Visually confirmed real, recognizable icons (gas pump, airplane, pharmacy cross) by decoding and inspecting them directly. Pasted into the same rasterized bitmap as road dots (~0.0056ms/paste β€” a whole-EEU 500km-span redraw with POIs on still takes only 0.150s). A real clutter bug found and fixed on the actual ISO: a dense real area (central Sofia) first rendered as an unreadable wall of overlapping icons β€” confirmed by inspecting a real screenshot, not assumed β€” fixed with a grid-occupancy declutter (52px cells) that caps rendered density regardless of true POI count; re-rendered and visually confirmed the fix produces genuinely readable icons.

Also this session ("v19 β†’ v20"): real POI display, unlocked by cracking POI.DB3's own Coordinate field the same session (a 64-bit Morton/ Z-order code). All 4,733,183 real POIs are extracted + decoded in the background (~8.6s total, never blocks opening the disc) and filtered per-viewport with the disc's own real MapZoomLevel visibility design β€” confirmed directly: a Sofia-area query returns a sensible real category mix (826 restaurants, 811 ATMs, 95 gas stations, ...) at a close zoom, correctly dropping to only long-range categories (airports, etc.) at a continent-wide zoom. Markers rasterize into the same bitmap as road dots (a wide-but-in-range viewport can cover 100k+ POIs, confirmed at 185,520 β€” individual canvas items at that count would risk the exact freeze "v17 β†’ v18" already fixed for roads); only labels are real canvas items, capped like city labels already are. Verified with a real running-app test: rendered pixels match the POI marker color exactly, and drop to zero after unchecking "Show POIs".

Also this session ("v18 β†’ v19"): 2 real, measured performance fixes. Road-name matching was being fully re-run over the entire pooled feature set on every single pan/zoom, even for tiles already matched by an earlier, overlapping load β€” now cached per (tile_id, tol_m) (MapData.name_caches), 293x faster on a real repeat-view test. App._to_canvas() was re-querying Tk for the canvas size on every single point/vertex it projects β€” now cached once per redraw. Both verified against the real ISO with before/after correctness checks (byte-identical results vs. ground truth), not just "didn't crash".

A real, severe bug found and fixed ("v24 β†’ v25"): every app close leaked its entire temp workdir

MapData extracts a working copy of the ISO's own files (all 5 layers, eeu.cty, POI.DB3, a search index) into a real tempfile.mkdtemp() directory per open β€” often 500MB-3GB+. MapData.close() deletes it correctly, but was previously only ever called when replacing an already-open ISO with a different one β€” never on normal app shutdown (no WM_DELETE_WINDOW handler existed). Closing the app via its window X button silently leaked the entire workdir, every session. Confirmed directly against a real system: 56 leaked directories, 146GB total, deleted after confirming no viewer instance was running. Fixed with a real close handler (App.on_close(), wired via root.protocol ("WM_DELETE_WINDOW", ...)) that closes self.data before the window exits.

"Show tile boundaries" checkbox ("v25 β†’ v26"), user-requested

Prompted directly by the real cross-tile adjacency gap found this session β€” a user-reported road connection crossed from one mg4 tile into a completely different one, a class of gap the adjacency resolver has no mechanism for at all. MapData.active_tile_bboxes() computes each currently-pooled tile's own bounding box from its already-decoded points (no extra disk I/O, no real per-tile grid bbox stored on-disc for these layers). Sanity-checked directly against the real cross-tile report: the 2 reported tiles' computed boxes meet almost exactly at the reported point's own coordinate. Draws each box as a thin outline rectangle, rasterized into the same single image as every other overlay (never individual canvas items β€” a wide viewport can pool 1,000+ tiles). Makes it easy to visually spot a dead-end point sitting right on a tile edge β€” a strong hint it's a candidate cross-tile connection worth reporting. OFF by default.

"Hide decode garbage" checkbox REMOVED ("v26 β†’ v27"), user-requested

User request (verbatim): "remove the hide decode garbage mechanism, i keep it always off because it doesnt help" β€” consistent with the checkbox's own history: real-ISO testing had already found the underlying oscillation filter net NEGATIVE for real dense-urban rendering (a real Sofia sample lost 6 whole named roads and badly truncated 13 more while removing only 5.74% of raw points), which is why it defaulted to unchecked before this removal. The checkbox and its handler are gone entirely β€” nothing in the running app can re-enable the filter anymore. MapData.trim_oscillation/set_trim_ oscillation() remain as a programmatic-only API (still exercised by test_map_viewer.py against the real ISO); decode_features()'s own detector also still exists in research/map_compressed_reader.py for research use. Only the UI surface is gone.

Real bug fixed ("v27 β†’ v28"): max_edge_m=200 was wrongly dropping real mg4/mg3 edges

User-reported ("missing points", then correctly "these 2 must be connected"): a real mg4 edge 1,518m long (a genuine divided-road carriageway transition) was being silently dropped by the adjacency resolver's own max_edge_m=200 default β€” calibrated only against dense mg2/ mp0 ground truth, never validated against the coarser mg4/mg3 layers where long real edges are normal. Auditing one feature found 28 real edges dropped this way. Fixed with a new MAX_EDGE_M_BY_LAYER dict (mg4/mg3: 5,000m; others unchanged at 200m), passed at both of MapData's own call sites. Verified with a live redraw test: rendered connected-roads line pixels increased at 2 zoom levels (667β†’1,183, 1,055β†’1,460) β€” more real edges now drawn.

A 2nd false-edge bug ("v28 β†’ v29"): the loosened mg3 cap above let a sentinel-value bug finally surface

Widening mg3's edge cap to 5,000m (previous entry) let a different, pre-existing bug through: _shared_value_edges() (research/ map_compressed_reader.py) never excluded the literal topology value 0 as a "not a real link id" sentinel β€” 5 earlier false edges (all mg1/mg2, 200m cap) happened to be masked by the tight distance cap alone, never by this exclusion actually existing. User right-clicked a real mg3 edge (tile 9237, points 61↔227, 4,555.3m apart, sharing only value 0) that the loose cap no longer caught. Fixed at the source: 0 is now excluded outright as a candidate link id, not relied on the distance cap to catch its symptoms. Re-validated: both human ground-truth tiles unchanged (mg2 20597 16/16, mp0 91124 16/18), all 5 original false edges still excluded, this case now excluded too β€” by never being proposed as a candidate at all.

The viewer stops extracting files to disk β€” reads directly from the ISO instead ("v29 β†’ v32", 3 sessions)

A 3-step arc, each step widening what's read directly out of the 6.48GB .ISO instead of being copied to a temp workdir first:

  • "v29 β†’ v30": the MAP_COMPRESSED tile layers (mp0/mg1-mg4). pycdlib.get_file_byte_extents() exposes a file's absolute byte range(s) inside the ISO; every file checked on this disc family (including mp0 at 2.14GB) is a single contiguous extent, so a tile decompresses by seeking straight into the ISO β€” byte-identical to reading an extracted copy (verified both at the raw-reader level and end-to-end: identical geo-index tile counts for every layer, the mg2 20597 ground-truth tile still resolves shift=1/"high"). load_heavy_layer()'s ~2.14GB mp0 copy step is gone entirely; only its (unchanged) geo-index build remains. The road editor (rns510_gui.py) is unaffected β€” editing needs a real mutable local copy, so its extract-then-edit flow is unchanged.
  • "v30 β†’ v31": eeu.cty, eeu.rd/.il/.iof, and eeu.ctr/eeuz.rt (the address-entry panel's own files) β€” each reader module gained its own small, dependency-free _open() (same duck-typing pattern, deliberately not centralized). rns510_core. MapProject gained a read_only=True flag that resolves files as ISO refs and refuses every edit/save call with a clear error. Verified: after a full load, workdir contains zero files β€” search, topology, and country-list loading all still work.
  • "v31 β†’ v32": POI.DB3 too β€” the last extracted file. sqlite3 can't seek a byte-window inside a bigger file, but Python 3.11+'s sqlite3.Connection.deserialize() loads a database straight from an in-memory buffer. Verified end-to-end: read 5.0s, deserialize() 1.65s, decoded the same 4,733,183 real POIs and 61 icons as always, workdir confirmed empty afterward. Real, accepted trade-off (confirmed with the user before implementing): POI.DB3's ~1.1GB stays resident in memory for the life of the connection instead of on disk. With this, the entire map viewer reads every file directly from the ISO; only the road editor still extracts anything.

tile_ids_in_bbox() gains 2 more real tests, then one of them is reverted ("v32 β†’ v37", 5 sessions)

  • "v32 β†’ v33": tile_ids_in_bbox() (decides which tiles to pool for the current viewport, called on every pan/zoom) used to test only a tile's single geo-index ANCHOR point against the query rectangle β€” a tile whose anchor sat just outside the viewport but whose real content still extended into it could be silently skipped. Now unions that with a 2nd test: does the tile's own real content bbox OVERLAP the query rectangle (a strict superset of the anchor test wherever both exist, so this can only ADD tiles, never drop any) β€” confirmed: the same small query that returned 1 tile via anchor-only returned 4 via the union. Built as 2 new background tasks (FAST_LAYERS right after load(); mp0's as a tail step of the existing heavy-layer load, deliberately AFTER mp0_ready, since build_bbox_index() takes minutes vs. build_geo_index()'s tens of seconds) β€” a missing/incomplete bbox index for any layer just falls back to anchor-only, never an error.
  • "v33 β†’ v34": a 3RD test β€” a tile's native, no-decompression geo- hint (a single coordinate bound, axis-ambiguous by construction, read straight out of the pre-table region with no tile decompression at all) β€” for fast partial coverage while the real bbox index is still building. Same "only ever adds tiles" philosophy. Built synchronously for FAST_LAYERS inside load() itself (2.7s combined, cheap enough not to need a background task).
  • "v34 β†’ v35": mp0_ready reordered to wait on the (much faster) native hint scan instead of the original build_geo_index() β€” street -level detail usable in ~8s instead of 35-55s+. Timed stage by stage on the real 194,705-tile file: native hints finish at 7.8s (53.7% coverage, mp0_ready now flips here); build_geo_index() completes at 158.9s, build_bbox_index() at 613.2s, both now pure background enhancements.
  • "v36 β†’ v37": REVERTED, a real user-reported performance regression, honestly measured and walked back. User report: tile search hung a long time on a first search at a new location. Root cause, measured directly: the native-hint test (either-axis, by construction) inflated pooled tile counts 4.5-10x over anchor-only at a realistic viewport (mg1 41β†’203, mg2 23β†’109, mg3 13β†’54, mg4 6β†’24) β€” confirmed structural across box sizes from ~170m to ~21km, not a large-viewport edge case. Every extra tile costs a real decode_tile() call β€” this was the reported slowdown, directly. Reverted, not patched: back to the 2-test union (anchor + bbox); mp0_ready back to waiting on build_geo_index() (flipping it after only the hint scan would leave mp0 "ready" but functionally near-empty for the ~150s gap until the real geo-index finishes β€” worse than honestly still loading). scan_native_geo_hints() itself is unchanged and still built (cheap, ~4.35s) for possible future use β€” it was specifically its use as a blind either-axis viewport filter that didn't hold up at real scale.

A 3rd and 4th false edge found and fixed ("v35 β†’ v36", "v37 β†’ v38")

  • "v35 β†’ v36": a contaminated 3-point clique, not a sentinel value this time. Same tile as the prior 2 false-edge fixes (mg3 9237): user-reported points 60↔211, 4,215.8m apart, sharing non-zero link-id value 11 with a 3rd point, 213 β€” but only the (211, 213) pair is real (23.4m apart); both (60, 211) and (60, 213) are false, all under mg3's loose 5000m cap so the distance filter didn't catch it either. _shared_value_edges() used to treat any clique of size ≀max_group as fully connected (every pair an edge) β€” wrong when an unrelated point just happens to also carry that value. Fixed: for cliques of size >2, keep only pairs within max(200m, 5Γ— the clique's own shortest pairwise distance), instead of accepting every pair uncritically. Re-validated: both ground-truth tiles unchanged, all 6 previously-confirmed false edges still excluded, this case excluded too, AND the real sibling edge (211, 213) confirmed to survive.
  • "v37 β†’ v38": a plain 2-point value collision (shared link-id value 100, no 3rd point, well under mg4's 5000m cap) β€” user- reported points 98 ("OKOLOVRASTEN PAT", Bulgarian for "Ring Road") ↔126, 772.8m apart. Gap alone isn't a safe signal (real validated edges reach a gap of 21), but cross-referencing gap AGAINST distance reveals a clean split that never overlaps in real data: large-gap real edges always have small distance, large-distance real edges always have tiny gap. This false edge is the one combination that never occurs for real (gap ratio 0.168, distance 772.8m, both large). Fixed: a new conjunctive filter β€” drop an edge only when BOTH its gap ratio exceeds 0.10 AND its distance exceeds 200m β€” so a short high-gap edge or a long low-gap edge each survive untouched. Re-validated: both ground-truth tiles unchanged, all 7 previously- confirmed false edges still excluded, this case excluded too, and the real 1,518m interleaved-parallel-chains edge confirmed to survive.

A 2nd, independent POI layer: real Sirius TravelLink data ("v38 β†’ v40")

  • "v38 β†’ v39": a byproduct of the firmware-side investigation (see Sirius TravelLink and North America Discs) β€” a real, 158,088-POI Sirius TravelLink database from a factory SWL disc, sharing enough of POI.DB3's own schema that poi_db_reader.load_poi_cache()/load_poi_partitions() work against it completely UNCHANGED. New: File > Load Sirius POIs... (a plain file picker β€” SIRIUS.DB3 is a standalone SQLite file, no extraction involved), a Show Sirius POIs checkbox, its own declutter grid (independent of the regular-POI one). Appended into the same rendered_pois list regular POIs use, so click-to-identify picks them up with zero extra code. Verified: the real Northern- California gas-station POIs ("BLOXHAMS SHELL", "CHEVRON", "UNION 76") decode and render correctly.
  • "v39 β†’ v40", immediate follow-up: real per-category icons for Sirius POIs too, not plain dots β€” load_poi_icons() works UNCHANGED against SIRIUS.DB3 since the same Image_BaseAttributes/ ImageBlob_BaseAttributes tables exist in its schema. Visually confirmed: partition 1 ("Fuel Station") decodes to a real, clean gas -pump icon. App._get_sirius_poi_icon() (identical shape to the regular-POI version), falls back to the plain teal dot only if a partition has no resolvable icon.

The canvas matches the real firmware's own screen shape β€” read live, nothing hardcoded ("v40 β†’ v41")

Investigated what's recoverable from FHDD6.FLI's own Java UI code β€” 123 real embedded ZIP/JAR headers and several PNG/GIF-tagged byte regions exist, but none panned out as directly usable "menu look" assets. A real overclaim caught and corrected, user-pushed ("why not"): an early pass claimed "156 entries, confirmed via a real, parseable Central Directory" β€” wrong, trusted too quickly. A real sequential walk (not just the EOCD's own summary fields) found only 1 entry genuinely self-consistent; only 4 real PK\x01\x02 markers exist in a 30,000-byte window near the EOCD, nowhere near 156 β€” this archive is fragmentary, not a complete JAR (and even an intact one would only yield more compiled bytecode, never image/layout data). The 3 PNG hits are false positives (no valid IEND); the GIF-tagged regions have a real header and palette but an invalid block marker right after β€” some proprietary format borrowing GIF's header convention, not real GIF data; PIL correctly refuses it.

What WAS solidly recoverable and is now implemented: the real screen resolution, 800Γ—480, confirmed 2 independent ways here (a real java.awt.Rectangle-shaped widget-bounds string, and 11 of the GIF-tagged regions' own palette dimensions agreeing) β€” a 3rd and 4th independent confirmation came later, see Theft Protection and SWL Disc: Boot Chain and Build System. New, fully dynamic, nothing hardcoded: rns510_iso.find_file() walks a real disc's own directory tree for a filename pattern (necessary since EU vs. North-America discs genuinely differ in folder layout β€” see Sirius TravelLink and North America Discs); swl_5238_reader.find_screen_resolution() scans raw FHDD6.FLI bytes for every widget-bounds string and returns the most common (width, height) pair (a real Counter, correctly ignoring a non-full-screen 100Γ—50 logo widget found the same way); File > Open Firmware ISO (real screen shape)... runs both in a background task and reshapes the canvas (letterboxed/pillarboxed, live on resize). Tested against all 3 real firmware ISOs on hand: 2 resolve to exactly (800, 480); the 3rd (the newest, largest NAR build) returns None β€” a real, honest per-build limitation (that build's compiled UI code just doesn't contain the resolution-revealing string), not a bug.

A real, still-open splash-screen lead followed further, same session: H_PQEE.FRG's per-variant "remainder" region (past the shared BSP stream, see HOST BSP and Vehicle Coding) is 772,577 bytes; 800Γ—480Γ—2 (RGB555) = 768,000 β€” a promisingly close match to the real leftover 4,577 bytes (already known to hold a real VW part number/"RNS-MID" text). Rendered as raw 800Γ—480 BGR555 (the same convention validated for the Sirius .DDB format): a clean, regularly-repeating textured/banded image, NOT random noise, but not recognizable either. FFT autocorrelation found a broad periodicity plateau near 700-742px, not one sharp value. Checked for zlib compression directly β€” zero real zlib headers anywhere in the remainder. 3 more real attempts, made DEFINITIVE: no plausible width/height header field in the first 64 bytes; RGB565 at 4 candidate widths produced the same banding, just a different hue (ruling out "right structure, wrong color format"); the decisive test β€” swept EVERY candidate width from 100 to 1600px and measured real row-to-row similarity (a standard, width-agnostic raw-image- forensics technique) β€” a true fixed-stride image shows one sharp minimum at its real width; this data shows none (the best 20 candidates differ by under 2% from each other). This rules out "raw, fixed-stride, 2-bytes-per-pixel bitmap, at ANY width" as a category, not just the specific widths/formats already tried β€” a materially stronger negative result. Whatever this data is, it needs either a fundamentally different structural hypothesis or independent ground truth (a real photo of an actual unit's boot screen), not further blind guessing.

2 separate search boxes β€” "Address" and "POI" ("v41 β†’ v42")

The single search box (road+city+POI all at once, MapData.search_combined()) is replaced in the UI by 2 separate boxes, each with its own Entry + Search button, stacked (Address: above POI:), sharing ONE results list below. New MapData methods, search_address() (roads+cities) and search_all_pois() (regular POIs + Sirius POIs together) β€” factored out of search_combined()'s own logic, left completely UNCHANGED (test_map_viewer.py's existing tests still call it directly). SearchHit gains a 4th real kind, "sirius" (renders [Sirius] instead of [POI]). Verified directly: search_address("SOFIA") returns 20 real hits; search_all_ pois("MCDONALD") returns real POI.DB3 matches; a loaded Sirius database searched for "CHEVRON" returns 5,682 real [Sirius]-tagged hits. A window screenshot confirms the 2-row layout renders as intended.

Road search results now zoom to the real segment size, not a flat default

SearchHit.jump_span_deg() previously had a real branch for "city" hits (scaled to roughly fit the city's own bounding box, clamped to [0.01, 2.0] degrees) but "road" hits just fell through to the caller's flat DEFAULT_JUMP_SPAN_DEG (0.3Β°, ~33km) regardless of whether the matched road was a short residential street or a long highway stretch β€” double-clicking ANY road result zoomed out to roughly the same county-sized area.

A later session's crack of eeu.rd's delta_long/delta_lat (each matched segment's own real bounding-box width/height, now exposed directly on core.RoadRecord) made a real fix possible: "road" hits now get their own branch, mirroring the city one β€” span = max(delta_lon, delta_lat) * 2.0, clamped to [0.01, default_span]. Since the real median segment diagonal is only ~350m, almost every road search result now zooms in tight enough to actually see the matched street (floor-clamped to 0.01Β°, ~1.1km); a genuinely long route segment still gets a proportionally wider span, up to the old flat default as a ceiling. Verified against real search results from the reference disc (core.MapProject.search("VIA") and others) β€” real named streets now produce ~0.01-0.012Β° spans instead of the old flat 0.3Β° for every single one.

Opening the same disc twice no longer re-pays the slow "Building geo-index..." cost

A still later session cracked the real on-disk structure behind the slow build_geo_index()/build_bbox_index() progress messages β€” a genuine k-d tree, found via DBAL.OUT disassembly (see MAP_COMPRESSED Container Format's own "Geo-index prefix" section for the full crack) β€” but 2 real gaps (the split coordinate's own numeric scale, and kd_pdecend/ kd_rdecend turning out to be a full rectangle-intersection range query rather than a simple point descent) meant swapping it in as the viewer's primary tile lookup right now would risk silently WRONG tile resolution. Not worth the risk for a speed win.

Shipped instead: a real, safe fix for the actual user-visible complaint. build_geo_index()/build_bbox_index() are unchanged; MapData now has a persistent geo_index_cache_dir (next to the script, NOT self.workdir, which is a fresh tempdir every run) and both call sites go through new build_geo_index_cached()/ build_bbox_index_cached() wrappers β€” a pickle cache keyed by the ISO's own identity (path + size + mtime) and the exact byte span being indexed, written atomically (tmp file + rename) so a killed process never leaves a corrupt cache entry. Re-opening the SAME disc a second time skips the decode-every-tile cost entirely.

Verified, not just implemented: cached and freshly-built results are bit-for-bit identical for both indexes. Real measured numbers against the reference ISO: a full MapData.load() dropped from 37.76s to 6.89s on a second run; a single layer's geo_index alone dropped from 1.30s to 0.010s (130x) once cached.

The "2 real gaps" above turned out to be a 4-byte reader bug β€” the k-d tree is now the primary bbox source

A still later session resolved both gaps the previous section declined to risk: cross-referencing eeu.mod's own schema (the disc's real data dictionary, see eeu.mod) found a 4-byte maxPclId field sitting between mapHeader and the k-d tree's own node array that the existing reader had never skipped β€” a plain off-by-4-bytes bug, not an external-schema wall, even though it produced symptoms (disconnected nodes, implausible edge values) that looked exactly like one. Fixing it makes a full tree traversal reach 100% of nodes and leaves on every layer tested, and edge turns out to use the SAME /100000-degree convention as every other coordinate on this disc β€” see MAP_COMPRESSED Container Format's "Geo-index prefix" section for the full validation numbers (median ~0.003-0.013Β° agreement against independently-decompressed content boxes, a real point-lookup matching 200/200 self-consistency checks).

MapData._build_layer_bbox_index() now tries mcr. build_kdtree_bbox_index() FIRST β€” a single structural tree traversal with NO tile decompression at all β€” falling back to the previous section's build_bbox_index_cached() only if the k-d tree can't be parsed for that layer (e.g. eeuz.fea, a different table format entirely) or comes back empty. Verified end-to-end against the reference ISO: load_bbox_indexes() (all 4 FAST_LAYERS) now takes ~2.1s total with 100% tile coverage on every layer (mg4 6,110/6,110, mg3 16,503/16,503, mg2 34,209/34,209, mg1 63,996/63,996 β€” previously 5,935-63,970, content-decode-dependent), and the full test suite (test_map_viewer.py) run end-to-end against both the old and new code produces identical results up through the point where an unrelated, pre-existing test failure (a canvas-pixel ground- truth check, confirmed present on the unmodified baseline too β€” not a regression from this change) halts the script in both cases. mp0 (the heaviest layer, 194,705 tiles) also goes through the new path as part of load_heavy_layer()'s own bbox-index step, reaching 194,705/194,705 (was 194,650/194,705) in under a second (previously up to several minutes).

Confirmed highways now render in their own color, using eeu.rd's newly-cracked highway bit

The connected-roads overlay previously colored every named road edge the same way (ROAD_COLOR_MAJOR, gold) regardless of real importance β€” the only signal available was n_vertices (a named run's point count), a crude proxy the module's own docstring already flagged as such ("no reliable road-class field exists"). Now that eeu.rd's roadInfo byte is fully cracked, a confirmed highway (bit 0 of that byte) gets its own color (HIGHWAY_COLOR, a strong green) and a thicker line β€” a REAL per-road signal, not a heuristic.

research/road_naming.py's RdCache now also vectorizes the roadInfo byte and exposes highway_names() β€” the set of real road names with at least one highway-flagged record, built with the exact same garbage-byte-clearing + numpy.unique() dedup technique distinct_names() already used. MapData.highway_names builds and caches this lazily (same policy as street_name_index_global), and the edge-coloring code in _redraw() checks it before falling back to the old name-length-based coloring.

Deliberately NOT touched: the per-vertex dot rendering. An earlier session fixed every dot to a single, unambiguous DOT_COLOR (red) at the user's own explicit request ("make the points red dots not gray ones") β€” this is a separate rendering path (the connected-roads line overlay, which already used multiple status colors) and doesn't revisit that decision.

Verified end-to-end against the real ISO: 578,946/8,809,081 records (6.57%) flagged, matching this project's own earlier-measured 5.9% route-like-name baseline closely; real route-numbered names (SS24, K4309, L606.3) correctly appear in highway_names(), an ordinary street name (MONGINEVRO) does not.

eeu.iof "nearby streets" lookup now fires on search-result jumps too, not just map clicks

MapData.nearby_streets_for_point() (the real eeu.iof/eeu.il "nearby street names" anchor lookup) already existed and was wired into the click-to-identify path (on_canvas_click() -> _append_nearby_streets_row()) β€” but searching for a road BY NAME and double-clicking the result to jump there (on_go_to_result()) never triggered it, even though the exact same data (rd_cache/search_project, already resident) is equally available at that point. Wired it in for "road"-kind hits, right after _jump_to() kicks off β€” the lookup only needs already-loaded in-memory arrays, so it runs synchronously rather than racing _jump_to()'s own async done() status-bar update.

(This investigation started from a still later session's firmware disassembly of eeu.iof's type byte β€” see that file's own wiki page β€” which turned out NOT to need any UI change: the newly-cracked type==1 "provisional count, recomputed at runtime" case covers only 7 of 31,318 real anchors (0.02%), confirmed directly against the reference disc, far too rare to justify special-casing. The actual gap found instead was this missing search-path wiring, unrelated to the type byte itself.)

Verified end-to-end against the real ISO: searching "MONGINEVRO" and jumping to one of its results resolves the real eeu.iof anchor "VITTORIO EMANUELE II" 1.7km away, with its full real cross-street list (VIA ROMA, SP233, SS24, VIA GIUSEPPE GARIBALDI, ...).

Real bugs found and fixed along the way, beyond the version-by-version entries above

  • _initial_scale() outlier sensitivity. A single decoded feature can legitimately be a long chain whose far end is a meaningful fraction of a degree away from the searched point. Letting one such outlier set the initial auto-fit zoom collapsed a real Tirana-area load to a near-country-wide scale, at which point far-away-but- genuinely-large cities (Bari, Ancona, Split β€” real Adriatic port cities, just hundreds of km away) out-ranked Tirana's own label. Fixed by clipping the auto-fit's bounding-box calculation to a generous multiple (2.5Γ—) of the requested jump span β€” only affects the one-time initial zoom choice, not what gets drawn once framed.
  • A minority of eeu.cty records have an implausibly huge bounding box. Found while tuning zoom-dependent city importance: a small number of records β€” so far only observed among postal-code-tagged sub-entries, e.g. "185 45, PEIRAIAS" (real Piraeus is a compact Athens-area port city) β€” have a bbox spanning tens of degrees (one observed case ~34 degΒ², most of the southern Aegean/Balkans for what should be one postal code's neighborhood), sitting right next to sibling entries for the SAME place with normal, tight boxes. Not a decode-offset bug (surrounding fields on the same record decode sanely, neighboring records at adjacent offsets are fine) β€” a genuine data-quality quirk in a minority of source records. Since "rank by bbox area" is this project's own established zoom- importance proxy (see eeu.cty), an unfiltered ranking lets these degenerate records win every low-zoom label slot by sheer spurious size. Mitigated with an optional max_area cap on CtyCache.query() (the map viewer passes CITY_MAX_AREA_DEG2 = 2.0, comfortably above any genuine city/admin area observed β€” TimiΘ™oara's is ~0.1) β€” the reader itself still returns the raw, unfiltered records, a filtering consumer opts in.
  • A related, subtler case: even with the above fix, a WIDE viewport can still rank a real (not degenerate) but much bigger surrounding admin area's box above the actual place at the view's center β€” observed directly: a rural municipality ("ZALL BASTAR") northeast of Tirana has a bbox that also legitimately covers central Tirana's coordinates. App._ensure_center_city_included() looks up whichever real .cty record most TIGHTLY contains the exact center point (smallest bbox among all containing records) and makes sure that one specifically is represented.
  • A corrupt fallback-path anchor near IaΘ™i: on a real mg3 tile, find_tile_anchor()'s "bruteforce" fallback path produced an outright corrupt anchor/feature thousands of km from the tile's true location. Defensively guarded (MAX_FEATURE_DRIFT_DEG=3Β°, drops any feature straying that far from its own tile's anchor) rather than root-caused β€” the underlying fallback path in research/map_compressed_reader.py could use tighter verification.
  • Long straight "spike" lines near Sofia, Bulgaria, root-caused and fixed at the source: 2 decoded features oscillating 24-33km between two near-fixed latitude bands, well within the 3Β° drift guard above (so that guard alone didn't catch it). Root cause: decode_features() running a block's declared point_count past where its real geometry ends, into misread non-coordinate bytes. Fixed by calling decode_features(raw, declen, trim_oscillation=True) in MapData.decode_tile(), which removes both Sofia spikes cleanly.
  • A follow-up near Turin, Italy found the same oscillating-overrun signature slipping through the original single-cutoff fix in 2 ways: a 2nd cluster hiding inside what the old code reported as a "clean, kept" prefix, and a cluster the old code missed entirely because it only ever looked for the FIRST one. decode_features()'s detector and reassembly were redesigned (multi-cluster scan + split into surviving sub-features instead of truncating). Re-running the same jump-detection check that originally found the Sofia bug, over the same Turin-area viewport: single-step jumps >1km across all pooled features dropped from 551 to 399 (max jump 10.4kmβ†’8.1km, total "wasted" jump distance roughly halved) β€” a real, measured improvement, though not total elimination: one specific tile's corruption remains uncaught and is still visible among the remaining jumps in this same viewport.

Known limitations (explicit, not oversights)

  • tile_ids_in_bbox()/find_tile_for_coord() are anchor-inside-box/ nearest-anchor, not exact tile-boundary containment (no tile extent was ever recovered) β€” a coordinate near a tile edge might miss a technically-adjacent tile's content until the viewport nudges further.
  • Road "importance" styling (thicker/darker for longer named runs) is a vertex/run-length proxy, not a real road-class field.
  • decode_features() still can't split one rendered chain back into per-named-road segments at the geometry level (the topology table that would allow this is still uncracked) β€” name_features() only ATTACHES names to vertex ranges of the geometry as already decoded, it doesn't re-cut the polyline. Roads render as unconnected per-vertex dots by default, so this no longer produces a visible wrong-line artifact β€” the underlying data gap is unchanged, only its visual symptom is gone (see Tile Geometry and Topology).
  • The eeu.cty huge-bbox data quirk above is capped for display, not explained β€” a future session could investigate which record types/fields predict it.
  • mp0 (the densest layer, build_geo_index() measured 34-55s on the reference disc) is not opened eagerly β€” it loads in the background right after "Open Map ISO" and folds into the same pooled view automatically once ready. Before that finishes, the view shows only whatever the 4 fast layers have β€” not an error, not a block β€” which can still look near-empty in an area that's sparse in every fast layer but has real content in mp0 (measured: a tight Sofia-area box has 0 tiles in mg1-mg4 but 1 tile/2 features/426 points in mp0). A real, un-eliminated rough edge: there was no way found to make mp0's own geo-index build meaningfully faster (build_geo_index() already reads every tile's exact byte range once, no wasted I/O), so a background window of some length is inherent to the current approach.

Non-GUI functional test: test_map_viewer.py (py test_map_viewer.py) β€” covers viewport-bbox math, in-memory caches, unified/split search, dynamic area loading, real names/cities actually appearing in decoded data, actual canvas rendering (item counts + label text) driven with real ISO data, a GUI-construction smoke test, and multi-layer pooling behavior end to end (including a real-ISO scale sweep proving monotonic/additive point-count growth and cache reuse on re-zooming). test_core.py covers the separate editing tool (rns510_gui.py).