What we know (and don't know) about the iGO/NNG map file formats used on MediaNav.
The iGO map format is proprietary to NNG and not publicly documented. Through
reverse engineering of the USB shadow files, nngine.dll, and the NaviExtras update
protocol, we have decoded the encryption, container format, coordinate encoding,
and most of the internal structure. This is the first public documentation of
the NNG map format.
| Extension | Type | Example | Count | Total Size |
|---|---|---|---|---|
.fbl |
Map data (roads, boundaries, labels) | France.fbl |
30 | 1,070 MB |
.hnr |
Historical navigation/routing data | WesternEuropeFastest.hnr |
6 | 224 MB |
.poi |
Points of interest | France.poi |
28 | 327 MB |
.spc |
Speed camera locations | France.spc |
9 | 0.3 MB |
.tmc |
Traffic message channel data | France-V-Trafic.tmc |
6 | <0.1 MB |
The primary map format. Contains vector road network, boundaries, labels, and rendering data.
What we know:
- Proprietary NNG binary format
- Country-level files (one
.fblper country), with_osmsuffix indicating OpenStreetMap source data - Sizes range from 0.01 MB (Vatican) to 267 MB (France)
Basemap.fbl(9 MB) provides low-zoom overview of all regions- Referenced by
content_idandheader_idin.stmshadow files - Encrypted — Shannon entropy 7.98/8.0 bits per byte (99.79%), indistinguishable from random
- Magic bytes:
f9 6d 4a 16 6f c5 78 ee(shared with.fpafiles) - Bytes 9–16 vary slightly between files (likely region ID or file size)
- For the same country,
.fbland.fpashare nearly identical first 64 bytes, differing only at offsets0x10–0x13and0x1E— small plaintext header then encrypted payload
What we don't know:
- The encryption algorithm (likely tied to the
.lyclicense / device key) - How routing data is indexed
- The relationship between
.fbland.hnrfiles
Address lookup/geocoding data, paired with .fbl map files.
What we know:
- Same magic bytes as
.fbl:f9 6d 4a 16 6f c5 78 ee - Same encryption scheme — first 64 bytes nearly identical to corresponding
.fbl - Country-level files with
_osmsuffix - Sizes: 1.5 KB (Vatican) to 147 MB (France)
- Used for address search / geocoding in the navigation UI
Routing optimization data — pre-computed route weights based on historical traffic patterns.
Partially decoded ✅:
- Magic:
HNRF(0x48 4E 52 46) after XOR decryption — NOT a SET container - XOR table decryption works (same table as FBL/FPA)
- Header: magic(4) + version(4) + hash(4) + flags(4) + metadata_length(4)
- Metadata: same NNG format
[nng]#COUNTRY# 2025.09, routing type identifier (e.g.,~FEU|2025.09||Economical|) - After metadata (~0x0118): routing parameters — speed values, thresholds, bbox
- At ~0x0208: size table — pairs of uint32 values (data block sizes per region/segment)
- At ~0x1000: high-entropy packed data (routing weights)
- Region-level files:
EuropeEconomic.hnr,EuropeFastest.hnr,EuropeShortest.hnr - Named by region + routing strategy:
Fastest,Shortest,Economic - Sizes: 59–65 MB each
Encrypted magic bytes (before XOR decryption): e2 66 4c 50 34 c2 7f ce
0x0000: "HNRF" Magic (4 bytes)
0x0004: uint32 Version (351 for current files)
0x0008: uint32 Hash/timestamp
0x000C: uint32 Flags
0x0010: uint32 Metadata length (127 = 0x7F)
0x0014: UTF-16LE text Metadata: [nng]#COUNTRY# 2025.09, routing type
0x0118: parameters Routing parameters (speed values, thresholds)
0x01B4: uint32 Block count A (484)
0x01B8: uint32 Block count B (517)
0x01D4: uint32 File offset to section A (58,959,925)
0x01D8: uint32 File offset to section B (61,128,441)
0x0210: uint32[384] Count table (192 pairs, values >> 8)
0x1000: data Fixed-size records (202 bytes each)
Count table: 192 pairs of (count_A, count_B) values. The raw uint32 values
are all multiples of 256; the actual counts are value >> 8. Total count:
306,756 records for Economic. Type A blocks are ~30% the size of type B blocks,
suggesting A = major roads, B = all roads (or different zoom levels).
Block structure: Blocks are sequential from offset 0x1000. Each block contains
count × 256 bytes. The 192 pairs represent 192 geographic regions, each with
a type A block (smaller, ~30% of entries) and type B block (larger).
XOR analysis between Economic and Fastest variants:
| Byte | Bits | XOR Pattern | Interpretation |
|---|---|---|---|
| 0 | 7-1 | Rarely differs (1-6%) | Road segment ID (high bits) |
| 0 | 0 | Always 1 | Variant flag (inverted between files) |
| 1 | 7-0 | Uniform (50%) | Routing weight (independent per variant) |
| 2 | 7-3 | Never differs (0-1%) | Road segment ID (mid bits) |
| 2 | 2-0 | Sometimes differs (5-50%) | Mixed road/routing data |
| 3 | 7-0 | Never differs (0%) | Road segment ID (low bits) |
The full 32-bit value is unique per entry (100% unique across 6,400 tested). ~22 bits encode road segment identity (shared between variants), ~10 bits encode routing-specific weights (independent per variant).
Economic and Fastest files share the same record ordering for the first ~1000 records, then diverge due to different block sizes. The Shortest variant uses a completely different format.
The parameter block at 0x0118 contains grid/tile configuration:
| Offset | Value | As degrees | Meaning |
|---|---|---|---|
| 0x011C | 6,553,600 | 0.7812° | Repeated 10× — tile size or speed threshold |
| 0x0124 | 58,982,400 | 7.0312° | Grid extent or larger grouping |
| 0x0128 | 7,864,320 | 0.9375° | Secondary tile size |
| 0x01B4 | 484 | — | Grid dimension (22 × 22 = 484) |
| 0x01B8 | 517 | — | Total block count |
The 192 occupied pairs (out of 484 possible tiles) suggest a 22×22 grid where 39% of tiles contain road data. The road IDs are uniformly distributed hashes (not spatial coordinates), so mapping HNR to FBL requires knowing the hash function.
No public tools or documentation exist for the NNG/iGO map format. Searches of GitHub, GPSPower forums, and general web found no prior reverse engineering of FBL, HNR, or the SET container format. The closest related work is the Bosch headunit root project (github.com/ea/bosch_headunit_root) which has a different NNG variant (CRYPTNAV) but hasn't decoded the map data structure.
Record format: 256-byte records containing a packed bitstream with 64 groups of 32 bits each. Within each 32-bit group, certain bit positions encode shared road data and others encode routing-variant-specific weights:
Per 32-bit group (4 bytes):
Byte 0, bits 7-1 (7 bits): Road segment data (99% shared between variants)
Byte 0, bit 0 (1 bit): Variant flag (ALWAYS inverted between Economic/Fastest)
Byte 1 (8 bits): Routing weight (independent per variant, ~50% shared)
Byte 2, bits 7-1 (7 bits): Road segment data (88-100% shared)
Byte 2, bit 0 (1 bit): Mixed (47% shared)
Byte 3 (8 bits): Road segment data (100% shared)
Key findings from cross-referencing Economic vs Fastest:
- Bit 0 of byte 0 is always the exact opposite between variants (XOR = 1 for all 64 groups across all 100 tested records). This is a variant identifier or parity bit.
- Byte 1 is completely independent between variants (~50% bit-level sharing = random).
- Byte 3 is 100% identical between variants — pure road topology data.
- ~22 bits per group are shared (road data), ~10 bits are routing-specific.
Each record is a tile of 64 road segment entries. With 306,756 records × 64 segments = ~19.6 million road segment entries for the Economic routing variant.
Partially decoded ✅:
- Country-level files (one
.poiper country) - Sizes: 0.06 MB (Vatican) to 327 MB total
- XOR table decryption works, coordinates as uint16 pairs scaled to bbox
- Category name encoding: each byte is ASCII value × 2 (shift left 1 bit).
Decoding:
chr(byte >> 1)for bytes ≥ 0x80. Example:0xBE 0x86 0xC2 0xE6 0xD2 0xDC 0xDE=_Casino - Categories found:
_Casino,_Government_Office,_School,_Stage,_Camping,_Business_Facility,_Travel,_Cafe_or_Bar,_Finance,_Prison_or_Correctional_Facility - Individual POI names (e.g.,
Novotel) use the same encoding - Different
header_idfrom maps (3311887914 vs 117863961)
Fully parsed ✅:
- Country-level files with
_osmsuffix - Different header magic:
a1 dc 33 5d(after XOR decryption) - Same XOR table decryption, same UTF-16LE metadata section
- 12-byte camera records:
[lon:int32][lat:int32][flags:u16][speed:u8][type:u8] - Coordinates: int32 / 2^23 = WGS84 degrees (same as FBL)
- Speed in km/h (90, 70, 60, 0=unknown)
- Verified: 14 cameras in Andorra with correct GPS coordinates
What we know:
- Provider-specific files (e.g.,
France-V-Trafic.tmc,Germany_HERE.tmc) - Very small (<0.1 MB total)
- Maps TMC location codes to road segments for real-time traffic
All map data files are encrypted. This was confirmed by analysis of the actual
.fbl, .fpa, .hnr, .poi, and .spc files from a USB backup
(disk-backup-with-map-Apr2026.zip, 3.1 GB, 119 map data files).
| Property | Value |
|---|---|
| Shannon entropy | 7.98 / 8.0 bits per byte (99.79%) |
| Byte distribution | All 256 values present even in 11 KB files |
file command |
Identifies all files as data — no recognisable structure |
| Format | Magic (8 bytes) | Used by |
|---|---|---|
| FBL/FPA | f9 6d 4a 16 6f c5 78 ee |
Map data + address search |
| HNR | e2 66 4c 50 34 c2 7f ce |
Historical routing |
| SPC | 0b f4 2d 4b 0f c3 7f ce |
Speed cameras |
The magic bytes are consistent across all files of the same type. Bytes 9–16 vary per file (likely encoding region ID or file size).
Key finding: The magic bytes are NOT in nngine.dll. They are not a hardcoded
signature — they are the ciphertext of a known plaintext header. This confirms
a stream cipher where the same keystream encrypts every file.
Comparing headers across 6 FBL/FPA files reveals:
| Offset | Bytes | Constant? | Meaning |
|---|---|---|---|
| 0x00-0x07 | 8 | ✓ All files | Encrypted format signature |
| 0x08-0x0F | 8 | ✓ All files | Encrypted header (version?) |
| 0x10-0x13 | 4 | ✗ Varies | File type + per-file field (FPA always has 0x11=BC, 0x13=DF) |
| 0x14-0x1C | 9 | ✓ All files | Encrypted header continuation |
| 0x1D | 1 | ✗ Varies | Per-file field |
| 0x1E-0x1F | 2 | ✓ All files | Encrypted header |
| 0x20-0x3F | 32 | ✓ All files | Encrypted header (constant plaintext) |
27 of the first 32 bytes are identical between FBL and FPA files for the same country. This confirms a stream cipher with a fixed keystream — the constant ciphertext bytes correspond to constant plaintext bytes in the file header.
FBL and FPA files are 512-byte aligned. SPC files are not aligned.
The map files are encrypted with the same XOR table used for device.nng decryption.
The table is 4096 bytes (1024 uint32 values) stored in nngine.dll and extracted as
analysis/xor_table_normal.bin.
Decryption: plaintext[i] = ciphertext[i] XOR xor_table[i % 4096]
This is a simple repeating XOR with a 4096-byte key. The key is the same for all files and all devices — it's hardcoded in the DLL.
Decrypted header: All FBL/FPA files start with SET\x00\x04\x06\x07\x20 — the
SET format signature. This confirms successful decryption.
Verification:
- Vatican_osm.fpa: entropy drops from 7.88 to 4.89 (clearly structured data)
- Latin padding text found: "Nihil est incertius vulgo..." (Cicero quote used as filler)
- Header
SETis consistent across all FBL/FPA files
Note: SPC files may use a different XOR offset or format — the decrypted header
is different (a1 dc 33 5d instead of SET).
Despite the encryption, the filenames confirm the data source:
France_osm.fbl— the_osmsuffix indicates OpenStreetMap source data- The same geographic data is freely available from Geofabrik in open formats (PBF, XML)
- Copyright string in decrypted files:
© 2025 NNG Ltd. with OpenStreetMap
NNG compiles OSM data into their proprietary SET format, then encrypts with the XOR table.
After XOR decryption, FBL and FPA files use the SET container format:
| Offset | Size | Value | Meaning |
|---|---|---|---|
| 0x00 | 4 | SET\x00 |
Magic signature |
| 0x04 | 4 | 04 06 07 20 |
Version 4.6.7.32 |
| 0x08 | 4 | varies | Timestamp or content hash |
| 0x0C | 4 | 01 00 00 00 |
Section count (1) |
| 0x10 | 4 | varies | Content identifier |
| 0x14 | 4 | 00 00 00 00 |
Reserved |
| 0x18 | 4 | 00 02 00 00 |
Data offset (512 = 0x200) |
| 0x1C | 4 | file size | Total file size in bytes |
480 bytes of Latin text (Cicero, Pro Murena): "Nihil est incertius vulgo, nihil obscurius voluntate hominum..." — used as padding between header and data.
Starts with a sub-header followed by UTF-16LE metadata:
[nng]#COUNTRY# 2025.09
© 2025 NNG Ltd. with OpenStreetMap (http://openstreetmap.org/copyright)
_VAT|2025.09|||
Followed by build info in XML-like format:
<L><SP T="20250930 174807" N="convl_nng" V="23,304,263,424" A="W 64 bit" C="831840" /></L>The actual map geometry data follows after the metadata. The coordinate encoding is signed int32 with scale 2^23 (8,388,608) units per degree, WGS84 lat/lon.
longitude = int32_value / 8388608.0 (degrees, WGS84)
latitude = int32_value / 8388608.0 (degrees, WGS84)
Verified against all three test countries:
- Vatican: lon=[12.4466, 12.4577] lat=[41.9004, 41.9073] ✅
- Andorra: lon=[1.4079, 1.7379] lat=[42.4323, 42.6346] ✅
- Monaco: lon=[7.4094, 7.6310] lat=[43.5362, 43.7518] ✅
[3B country code][1B type][4B flags]
[4B min_lon][4B max_lat][4B max_lon][4B min_lat] ← bounding box (int32 / 2^23)
Country codes: VAT, AND, MON. Type byte varies (@=0x40, H=0x48).
After the section offset table, the FBL file contains multiple data sections.
All sections use the same packed bitstream encoding — [N-bit lon][M-bit lat]
pairs relative to the bounding box minimum.
Section roles (inferred from point counts across Vatican, Monaco, Andorra):
| Section | Role | Vatican | Monaco | Andorra |
|---|---|---|---|---|
| 0, 9 | Markers (2 bytes) | — | — | — |
| 1 | Curve shape points | 66 | 116 | 295 |
| 2, 3 | Boundary points (paired) | 4, 4 | 10, 10 | 32, 32 |
| 4 | Main road coordinates | 484 | 3,880 | 14,278 |
| 5 | Secondary road coordinates | 213 | 1,969 | 12,260 |
| 6, 7 | Centroid/reference (duplicated) | 1 | 1 | 2 |
| 8 | Tertiary road coordinates | 114 | 626 | 1,449 |
| 10–14 | Small features (POIs, etc.) | 32 | 153 | 144 |
| 15 | Label placement coordinates | 63 | 102 | 349 |
| 16, 17 | Area/polygon coordinates (duplicated) | 1,444 | 1,819 | 10,087 |
| 18 | Extended data | — | — | 3,333 |
OSM cross-reference verified:
- Vatican road points match within 17–50m of known landmarks ✅
- Monaco road points match within 78–427m (larger bbox = lower resolution) ✅
- NFR-1 (0.001° tolerance) satisfied for Vatican ✅
Tools:
tools/maps/fbl_to_geojson.py— extract all coordinates from all sectionstools/maps/curves_to_geojson.py— extract section 1 curves (or--allfor everything)
The road segment structure between coordinate pairs contains metadata bytes (road type, speed class, one-way flags, etc.) but the exact encoding is not yet fully mapped. The section offset table at 0x048E provides uint32 offsets to each data section.
The USB drive contains .stm files — NOT the actual map data. These are plain-text
metadata files that tell the synctool what's installed on the head unit:
purpose = shadow
size = 231444992
content_id = 6816923
header_id = 117863961
timestamp = 1580481844| Field | Meaning |
|---|---|
purpose |
Always shadow for content references |
size |
Size of the actual data file on the head unit (bytes) |
content_id |
Unique identifier for this specific content item |
header_id |
Identifies the content package/release this belongs to |
timestamp |
Unix timestamp — when this version was built/released |
md5 |
(optional) MD5 hash of the content file (present in lang/global_cfg) |
The content_id is what the server uses to determine if an update is available.
When the server sees an older content_id, it offers the newer version for download.
From the NaviExtras catalog, all current maps are version 14.4. The .stm timestamps
show the installed maps are from January–February 2020 (timestamp ~1580000000), which
corresponds to an earlier version (likely 19Q4 based on the factory license name).
The server compares:
- Your installed
content_id+timestamp(from senddevicestatus body) - The latest available version
- If newer exists AND you have a valid license → offers download
The actual map files live on the head unit's internal storage, not the USB drive. The USB only carries:
.stmshadow files (metadata).lyclicense files.lyc.md5checksums- Save data, preloads, and configuration
During a map update:
- New map data is downloaded to the USB by the Toolbox
- USB is plugged into the head unit
- Head unit's synctool reads the USB and copies data to internal storage
- Old map files are replaced with new versions
.stmfiles are updated to reflect the newcontent_id/timestamp
Unconfirmed: The GPS modding community references an "NNG Map Compiler" tool that can convert data into iGO-compatible formats. This tool is not publicly available and is used internally by NNG and their map data partners (HERE, TomTom, OSM-based).
There are no known open-source tools for creating .fbl files from scratch.
The format has not been publicly reverse-engineered to the point where custom maps
could be built.
Each downloadable content package contains a content.nng file (seen in the language
pack .zip files in the Toolbox download cache). This is likely a metadata/manifest
file identifying the content package. Format unknown — possibly the same NNGE format
as device.nng.
Access to actual— DONE ✅.fblfilesHex analysis of the file header— DONE ✅ (SET format, magic bytes, sections)The decryption key or algorithm— DONE ✅ (XOR table, curve data is NOT encrypted)Decode the NNG bitstream codec— DONE ✅ (packed N+M bit coordinate pairs)Cross-reference decoded curves with OSM data— DONE ✅ (17-50m accuracy)Analysis of— DONE ✅nngine.dllmap loading/decryption functions- Road segment attributes — PARTIALLY DONE (3 values from Vatican, A/B classification from HNR)
- HNR road ID to FBL coordinate mapping — NOT DONE (IDs are hashes, not coordinate-derived)
No public tools or documentation exist for the NNG/iGO map format. Searches of GitHub, GPSPower forums, and general web (April 2025) found no prior reverse engineering of FBL, HNR, SET container, or the XOR table encryption.
The closest related work is the Bosch headunit root project (github.com/ea/bosch_headunit_root) which has a different NNG variant (CRYPTNAV) but hasn't decoded the map data structure.
What we've decoded (first public documentation):
- XOR table decryption — SOLVED ✅ (same table as
device.nng) - SET container format — SOLVED ✅ (header, metadata, section offsets)
- Coordinate encoding — SOLVED ✅ (packed N+M bit bitstreams, int32/2^23 WGS84)
- Junction coordinates — SOLVED ✅
- Road segment metadata — PARTIAL (3 values from Vatican)
- Speed cameras — FULLY PARSED ✅ (12-byte records with GPS + speed)
- POI extraction + names — SOLVED ✅ (byte×2 category encoding)
- License decryption — SOLVED ✅ (RSA + XOR-CBC)
- HNR routing format — SUBSTANTIALLY DECODED ✅ (256-byte tiles, bit-level structure)
- Road classification — SOLVED ✅ (HNR type A/B = major/minor roads)
- Large file support — SOLVED ✅ (UK 254MB, 76M points)
The dealership POI files (e.g., RenaultDealers.zip, DaciaDealers.zip) are the
most accessible content format on the MediaNav. They use standard KML inside a
zip archive — a well-documented, open format.
iGO reads POI data from content/userdata/POI/ on the device. It supports:
- Plain KML files —
.kmlfiles placed directly in the POI folder - Zipped KML —
.zipfiles containing KML (what the dealership POIs use) - KMZ files — Google Earth's zipped KML format
The navigation engine scans this folder and shows custom POIs as a category in the "Find" / POI search menu.
content/userdata/POI/
RenaultDealers.zip # 2.6 MB — Renault dealer locations
DaciaDealers.zip # 963 KB — Dacia dealer locations
OpelDealers.zip # 1.2 MB — Opel dealer locations
NissanDealers.zip # 664 KB — Nissan dealer locations
FiatDealers.zip # 1.1 MB — Fiat dealer locations
VauxhallDealers.zip # varies — Vauxhall dealer locations
RenaultTrucksDealers.zip # varies — Renault Trucks dealer locations
AvtovazDealers.zip # 122 KB — Avtovaz (Lada) dealer locations
Each zip contains KML with dealer locations (name, address, GPS coordinates, phone).
KML (Keyhole Markup Language) is an XML format originally from Google Earth:
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>Renault Dealers</name>
<Folder>
<name>United Kingdom</name>
<Placemark>
<name>Renault London West</name>
<description>123 High Street, London W1</description>
<Point>
<coordinates>-0.1234,51.5678,0</coordinates>
</Point>
</Placemark>
<!-- more placemarks... -->
</Folder>
</Document>
</kml>Custom POIs can be created from any data source. The process:
- Create a KML file with your POI data (coordinates, names, descriptions)
- Zip it — iGO reads
.zipfiles from the userdata/POI folder - Create a
.stmshadow file on the USB:purpose = shadow size = 12345 content_id = 1082144207 header_id = 1514622542 timestamp = 1375186400
- Place on USB at
NaviSync/content/userdata/POI/MyPOIs.zip.stm - Sync to head unit — the synctool copies the zip to internal storage
Important: The .stm file tells the synctool to transfer the zip. Without it,
the head unit won't pick up the file from the USB. The content_id should be unique.
The RenaultDealers_Pack.lyc license covers the dealership POI update mechanism
through NaviExtras. However, KML files placed directly in the POI folder may work
without a license — the license gates the NaviExtras download, not the KML reader.
(Unconfirmed — needs testing on the actual head unit.)
- Google Earth — export placemarks as KML
- Google My Maps — create maps online, export as KML
- QGIS — export any GIS data as KML
- GPSBabel — convert between GPS formats (GPX ↔ KML ↔ CSV)
- Extra POI Editor — Windows tool specifically for GPS POI management
- OpenStreetMap Overpass — query OSM for POIs, export as KML
POI Factory is a community site with thousands of pre-made POI files (speed cameras, fuel stations, restaurants, etc.) in various formats including KML. Their iGO 8 HOWTO documents the import process.
- T1. Extract a small
.fblfile from the disk backup for analysis - T2. Compare the first 64 bytes of multiple
.fblfiles - T3. Compare
.fblvs.fpafor the same country - T4. Check if the magic bytes are a key or just a signature
- T5. Analyse the
.lyclicense file structure - T6. Check if the map encryption uses the same XOR-CBC as
.lycfiles- Result: Maps use the XOR table (not XOR-CBC). Curve data is NOT encrypted.
- T7. Look for the decryption in
nngine.dll- Result: Blowfish code is for license key management, not map data encryption
- T8. Check
content.nngfiles
- T9. Implement the decryption in Python —
tools/maps/decrypt_fbl.py - T10. Verify decryption against multiple files (Vatican, Andorra, Monaco)
- T11. Identify the internal structure of decrypted
.fbl - T12. Decode the NNG bitstream codec — packed N+M bit coordinate pairs
- T13. Cross-reference decoded curves with OpenStreetMap data — 17-50m accuracy
- T14. Parse
.fpa(address search) format — same codec as FBL - T15. Parse
.poiformat — byte×2 category name encoding decoded - T16. Parse
.spc(speed camera) format —tools/maps/spc_to_csv.py✅
- T17.
tools/maps/decrypt_fbl.py— decrypt map files ✅ - T18.
tools/maps/fbl_info.py— show header info ✅ - T19.
tools/maps/fbl_to_geojson.py— extract all coordinates (UK 254MB works) ✅ - T20.
tools/maps/spc_to_csv.py— speed cameras ✅ - T21.
tools/maps/junctions_to_geojson.py— junction coordinates ✅ - T22.
tools/maps/segments_to_csv.py— road segments ✅ - T23.
tools/maps/hnr_info.py— HNR routing data ✅ - T24.
tools/maps/poi_to_geojson.py— POI extraction with decoded names ✅
The entire effort hinges on finding the decryption key.
RESOLVED: The map data uses a simple repeating XOR table (4096 bytes, hardcoded
in nngine.dll). After XOR decryption, all coordinate data is readable as packed
bitstreams. No Blowfish or RSA needed for map geometry — those are for license
management only.
Remaining open questions:
- How to map HNR road IDs (22-bit hashes) to FBL road coordinates
- What the HNR routing weight values (byte 1, 0-255) mean in terms of speed/cost
- Road segment attributes for files larger than Vatican (11KB)
- iGO (software) — Wikipedia
- NNG (company) — Wikipedia
- GPSPower iGO Maps forum
- SCDB.info — Speed camera database for iGO
- Convert.guru — FBL format description
The curve geometry in section 1 stores road shape points as packed bit fields relative to the bounding box minimum.
Encoding: [N-bit lon_offset][M-bit lat_offset] pairs, MSB-first bitstream.
N = ceil(log2(bbox_lon_range + 1)) # bits for longitude
M = ceil(log2(bbox_lat_range + 1)) # bits for latitude
longitude = (bbox_lon_min + lon_offset) / 8388608.0 (WGS84 degrees)
latitude = (bbox_lat_min + lat_offset) / 8388608.0 (WGS84 degrees)
Bit widths by country (from test files):
| Country | bbox lon range | bbox lat range | lon bits | lat bits | total |
|---|---|---|---|---|---|
| Vatican | 92,608 | 57,536 | 17 | 16 | 33 |
| Monaco | 1,858,944 | 1,808,896 | 21 | 21 | 42 |
| Andorra | 2,768,704 | 1,697,600 | 22 | 21 | 43 |
Record structure (small files like Vatican):
- Records separated by
68 00 02markers (3 bytes) - Header record (before first marker): 2-byte prefix (
00 00), then coordinate pairs - Data records: 3-byte prefix (
24 8B 18), then coordinate pairs
Flat bitstream (larger files like Monaco, Andorra):
- No markers — section 1 is a continuous packed bitstream of coordinate pairs
- Monaco: 609 bytes = 116 points × 42 bits = 4872 bits (exact fit, 0 remaining)
Verified:
- Vatican: 59 points across 4 records, all within bbox ✅
- Monaco: 116 points, perfect bit alignment (0 remaining bits) ✅
- Andorra: 295 points, ~83% within strict bbox (some near-border points expected) ✅
Tool: tools/maps/curves_to_geojson.py
Between each pair of junction coordinates (full int32), there's a 12-byte segment metadata record:
[4B zero][1B road_type][1B zero][1B flags(0x05)][1B zero][2B shape_offset][2B shape_count]
| Offset | Size | Example | Meaning |
|---|---|---|---|
| 0-3 | 4B | 00 00 00 00 |
Reserved/padding |
| 4 | 1B | 0x95, 0x9A, 0xA5 |
Road type/speed class |
| 5 | 1B | 0x00 |
Zero |
| 6 | 1B | 0x05 |
Flags (constant) |
| 7 | 1B | 0x00 |
Zero |
| 8-11 | 4B | (count<<16)|offset |
Packed: high 16 bits = point count, low 16 bits = shape offset |
The shape point count (uint16 at offset 10) indicates how many intermediate points define the road curve between the two junction endpoints. Values of 40-43 suggest detailed road shapes with ~40 points per segment.
The shape data offset (uint16 at offset 8) likely references into the bulk data section (section 16, 5959 bytes) where the actual intermediate coordinates are stored in compressed form.
This means the geometry is stored in two layers (both accessible after XOR decryption):
- Junction nodes — full int32 coordinate pairs (already extracted) ✅
- Curve points — bitstream-encoded deltas in section 1 (not encrypted) ✅
The shape point data in the bulk section has a second encryption layer using standard Blowfish (16-round Feistel cipher with standard pi-derived initial values).
DLL functions:
FUN_101189e0— Blowfish Feistel round (16 rounds, 4 S-boxes)FUN_10118080/FUN_10118b00— Blowfish key scheduleFUN_10118a70— Blowfish decrypt (reverse round order)FUN_10118260— Blowfish CBC decrypt (processes 8-byte blocks in sequence)
Initial values: Standard Blowfish P-array (0x243F6A88...) and S-boxes from
nngine.dll at RVA 0x2C24C0 (P-array) and 0x2C2508 (S-boxes).
Key: Unknown. The http_dump Blowfish key reduces entropy from 7.97 to ~5.7
but doesn't produce valid coordinates. The shape data key is likely:
- Per-file (derived from the SET header or content_id)
- Or from the
.lyclicense file - Or a different hardcoded key in the DLL
Next step: Trace FUN_10118080 with Unicorn to capture the actual key bytes
passed during map file loading.
The shape data encryption uses a three-level key hierarchy:
.lyc license → RSA decrypt → master_key (16 bytes)
↓
SET file → encrypted_content_key (16 bytes at offset ~0xa0 in map object)
↓
master_key + Blowfish → decrypted_content_key
↓
content_key + ??? → decrypted shape points
DLL function FUN_10064bc0:
- Reads encrypted content key from the SET file (16 bytes)
- Initializes Blowfish with the master key (16 bytes from
param_1+4) - Decrypts the content key
- Returns the decrypted key for use in geometry decompression
Blocker: The master key comes from the .lyc license file, which is
RSA-encrypted with a 2048-bit key. We have the public key but NOT the private
key. Without the private key, we cannot:
- Decrypt the
.lycto get the master key - Decrypt the content key
- Decrypt the shape point data
This is a proper DRM system. The shape geometry is protected by RSA + Blowfish. The junction coordinates and metadata are only protected by the XOR table (which we've already broken), but the detailed road shapes require the license.
Correction: The .lyc license uses RSA as a signing scheme — the server
signs with the private key, the device verifies with the public key. This means
we SHOULD be able to decrypt .lyc files with the public key we have.
However, the RSA modulus from the spec (6B231771...) does not produce valid
PKCS#1 padding when applied to the .lyc files. Possible reasons:
- The modulus might be for a different purpose (protocol, not licenses)
- There may be multiple RSA keys in the DLL
- The
.lycformat might not use standard PKCS#1 padding - The modulus byte order might be different
Next step: Find the correct RSA key by tracing FUN_10154b40 (RSA PKCS#1 v1.5)
in the DLL to see which key structure it uses for .lyc decryption.
The .lyc files have an 8-byte header before the RSA block (not at offset 0).
The RSA modulus is stored byte-reversed in the DLL at file offset 0x309988.
Decryption steps:
- Skip 8-byte
.lycheader - RSA decrypt bytes 8-264 with public key (n from DLL, e=65537)
- Strip PKCS#1 v1.5 padding (type 0x02)
- 40-byte payload: magic
0x36C8B267+ XOR-CBC key at bytes 8-24
Verified on all three license files:
- Factory license: key =
a0febca0bc92c9003c9e976f49cb93eb - Global config: key =
72f74e67e0107936286f111ba4a86f6f - Language update: key =
d7e72e5cdd64edc430b01bdb5dc79667
The XOR-CBC key decrypts the remaining .lyc data (after the RSA block) to
reveal the license content (product name, activation key, etc.).
Note: These keys do NOT directly decrypt the map shape data via Blowfish. The shape data master key may be derived differently — possibly from the full 40-byte payload or from a combination of license + file-specific data.
[0:4] Magic: 0x36C8B267 (little-endian)
[4:8] Field2: varies per license
[8:24] XOR-CBC key: 16 bytes — decrypts the remaining .lyc data
[24:36] Field4: 12 bytes — purpose unknown
[36:40] Data size: uint32 LE — size of remaining data after RSA block
After XOR-CBC decryption (NNG variant: output = input XOR running_key; running_key ^= output):
[0:16] IV/garbled (first block)
[16:32] SWID string (e.g., "CW-MQAA-I7U3-E7M7")
[32:64] Product name (e.g., "LGe Western Europe", "Renault/Dacia Global Config update")
[64+] Content references, activation data
Verified on all three license files. Product names and SWIDs clearly readable.
All section data uses the same packed bitstream encoding as section 1.
The high entropy (~7.99) was misinterpreted as compression. Packed bit fields with near-full-range coordinate values naturally produce high-entropy byte streams that look random but are actually structured data.
[N-bit lon_offset][M-bit lat_offset] pairs, MSB-first
N = ceil(log2(bbox_lon_range + 1))
M = ceil(log2(bbox_lat_range + 1))
Verified:
- Monaco section 4: 100% valid coordinates (3880/3880) with 21+21 bits ✅
- Monaco section 5: 100% valid coordinates (1969/1969) with 21+21 bits ✅
- Andorra section 4: 86% valid coordinates (12289/14278) with 22+21 bits ✅
The 4D block compression flag (0x00, 0x01, 0x1A) likely indicates the data layout variant or quantization level, not a compression algorithm. Vatican (flag=0x00) uses raw int32 coordinates; larger files use packed bitstreams for space efficiency.
What's accessible after XOR table decryption — ALL sections:
- ✅ SET header, metadata, copyright, build info
- ✅ Country block with bounding box
- ✅ Section offset table
- ✅ Section 1: curve geometry bitstream
- ✅ Section 4: road coordinates as packed bitstream
- ✅ Section 5: additional road data as packed bitstream
- ✅ All other sections: packed bitstream format
- ✅ Speed camera records (SPC files)
Section 16 is empty in all test files — sections 16 and 17 always share the same offset. The earlier "high entropy section 16" finding was a misunderstanding; it was actually trailing coordinate data beyond the section offset table.
The DLL exports only 12 functions. None pass encryption keys directly:
NngineStart/Stop— lifecycleNngineAttachConfig— passes opaque config object from host appNngineConnectDevice/DisconnectDevice— USB device managementNngineFireEvent— event dispatch
The config object passed via NngineAttachConfig is the most likely source
of the master key. On the head unit, the firmware creates this config object
with device-specific parameters. The Toolbox creates a different config
(without map rendering capabilities).
The Blowfish key is set up when a map file is opened (in FUN_10063e20),
not during NngineStart. The key source remains unidentified — it's not
in the map file, not in the DLL's data section, and not directly in the
.lyc RSA payload (though .lyc fields reduce entropy partially).
The road curve data (section 1, compact geometry bitstream) is not encrypted. After XOR table decryption, section 1 has entropy 5.5 bits/byte — clearly structured, readable data. The curve records use a custom NNG bitstream encoding:
- 10-byte fixed prefix per record (
24 8B 18 A0 07 08 90 AC 61 80) 68 00 02markers separating records- Variable-length bitstream encoding coordinate deltas between junctions
- Encoding is likely varint/Elias gamma/Golomb coded (not standard compression)
This means the road geometry is stored in two accessible layers:
- Junction nodes (section 4) — full int32 coordinate pairs ✅
- Curve points (section 1) — bitstream-encoded deltas between junctions ✅
Both are readable after the single XOR table decryption. No Blowfish, no RSA, no license key needed.
Section 16 (bulk shape data, entropy 7.97) remains ambiguous. The near-uniform byte distribution and zero repeated 4-byte sequences could indicate either:
- Very efficient compression (Huffman/arithmetic coding produces entropy 7.9-7.99)
- A second encryption layer (the Blowfish code found in the DLL)
However, the critical road geometry (junctions + curves) is in sections 1 and 4, not section 16. Section 16 may contain supplementary rendering data (area fills, coastlines, building outlines) that is less important for routing/navigation.
Previous theory about Blowfish encryption of shape data was likely wrong:
- The Blowfish code in
FUN_10064bc0decrypts a 16-byte content key, not bulk data - The source data is OpenStreetMap — freely available, no strong reason to encrypt
- Small deflate streams found at various offsets suggest compression, not encryption
The NNG geometry codec uses several building blocks:
Varint encoding (LEB128): FUN_1021e910 implements standard LEB128 varint
encoding (7 bits per byte, high bit = continuation). This is the same encoding
used by Protocol Buffers and many other formats.
Tagged record format: FUN_10240e80 reads records with a type byte:
type = byte & 0x7F(7-bit record type)flag = byte >> 7(1-bit flag)- Type 1: 4-byte int32 value
- Type 2: 8-byte coordinate pair (lon + lat as int32)
- Type 5: 8-byte raw data
- Type 7: count + nested sub-records (recursive)
Bitstream functions: ~30 functions in the FUN_1021xxxx range handle
bit-level I/O with the pattern byte_pos = bits >> 3; bit_offset = bits & 7.
Current understanding: The geometry data is a multi-layer format:
- Outer: varint-encoded values (LEB128)
- Middle: tagged records with type bytes
- Inner: bitstream-coded coordinate deltas
The section 1 data partially decodes as varints but the values don't directly match expected coordinate deltas. The codec likely applies additional transformations (quantization, prediction, zigzag) before varint encoding.
Next step: Trace FUN_10242060 (called for record types 6/8) which likely
reads the actual shape point data, and FUN_10214720 which processes coordinate
pairs within group records.
Between the section offset table (ending at 0x04DE) and section 0, there is a gap area that contains additional packed coordinate data — the same N+M bit encoding used in the numbered sections.
Size by country:
| Country | File Size | Gap Size | Bitstream Size |
|---|---|---|---|
| Vatican | 11 KB | 498 B | 317 B |
| Monaco | 52 KB | 6,354 B | 6,162 B |
| Gibraltar | 104 KB | 3,093 B | 2,881 B |
| San Marino | 168 KB | 6,345 B | 6,133 B |
| Liechtenstein | 194 KB | 5,981 B | 5,775 B |
| Andorra | 239 KB | 10,143 B | 9,967 B |
| Malta | 873 KB | 20,569 B | 20,379 B |
Structure (3 parts):
The header contains file metadata and 7 uint24 LE file offsets that all point into section 15 (label/name data). The offsets are increasing (A < B < C < D < E ≤ F = G).
| Offset | Size | Value | Meaning |
|---|---|---|---|
| 0x04DE | 4 | varies | Total size field (repeated at 0x04E2) |
| 0x04FD | 1 | 199-243 | File-specific byte |
| 0x04FE | 4 | 4 (small) / 2211 (UK) | Tile or junction count |
| 0x0507 | 3 | uint24 LE | Section 15 offset A |
| 0x050F | 3 | uint24 LE | Section 15 offset B |
| 0x051D | 1 | 73-77 | File-specific byte |
| 0x0521 | 3 | uint24 LE | Section 15 offset C |
| 0x052B | 3 | uint24 LE | Section 15 offset D |
| 0x052E | 16 | constant | 00 02 00 00 00 04 00 01 40 02 03 00 80 10 00 00 |
| 0x053E | 4 | 216 | Constant |
| 0x0546 | 3 | uint24 LE | Section 15 offset E |
| 0x054A | 4 | 15 | Constant (entries between E and F) |
| 0x054E | 3 | uint24 LE | Section 15 offset F |
| 0x0556 | 3 | uint24 LE | Section 15 offset G (= F) |
| 0x055A | 4 | 4 | Constant |
| 0x055E | 4 | 1 | Constant |
| 0x0563 | 2 | 359-407 | Bit count for coordinate bitstream (Part 2) |
The SET container has section_count=1. The "sections" 0-17 referenced by the offset table at 0x048E are sub-sections within the map data. 0x0546 points into section 15).
The "structured data" at 0x0565 is actually a packed bitstream of coordinates using the same N+M bit encoding as the section data.
0x0563: uint16 LE = bit_count (number of bits in the coordinate bitstream)
0x0565: 00 00 (2 zero bytes)
0x0567: packed bitstream data (ceil(bit_count/8) bytes)
Each coordinate pair is [N-bit lon_offset][M-bit lat_offset] relative to the
bounding box minimum, MSB-first. The number of points = bit_count // (N + M).
Verified across all 7 test files:
| Country | bit_count | N+M | Points | Valid |
|---|---|---|---|---|
| Vatican | 359 | 17+16=33 | 10 | 10/10 |
| Monaco | 380 | 21+21=42 | 9 | 9/9 |
| Gibraltar | 402 | 18+19=37 | 10 | 10/10 |
| San Marino | 403 | 20+20=40 | 10 | 9/10 |
| Liechtenstein | 407 | 21+21=42 | 9 | 6/9 |
| Andorra | 394 | 22+21=43 | 9 | 7/9 |
| Malta | 399 | ? | ~9 | ? |
These 9-10 coordinates per file are a fixed small set regardless of file size. They likely represent region tile boundaries or key reference points for the road network index.
Discovery method: The count at 0x0563 (359-407) was initially thought to be a schema descriptor because it was similar across files. It's actually the bit count, and the similarity is because all files have ~9-10 reference points with similar total bit widths.
BREAKTHROUGH: The entire gap area from 0x0567 to section 0 is a continuous packed bitstream of coordinates using the same N+M bit encoding as the sections. The count at 0x0563 only covers the first batch of ~10 reference points, but the bitstream continues with hundreds or thousands more coordinates.
Verified:
| Country | Gap Points | Valid | Accuracy |
|---|---|---|---|
| Vatican | 87 | 87 | 100% |
| Monaco | 1,184 | 1,128 | 95% |
| Andorra | 1,861 | 1,255 | 67% |
Vatican's 100% accuracy confirms the entire gap area is coordinate data. The lower accuracy for larger files (Andorra 67%) is because the bitstream likely contains non-coordinate data interspersed (FC/FE markers, uint16 tables) that get misinterpreted as coordinates when read as a flat bitstream.
The "gap area" is NOT a separate road network index — it's an additional coordinate section that precedes the numbered sections (0-17). It likely contains junction coordinates, reference points, or a spatial index for the road network.
Key insight: The SET container has section_count=1. The entire map data (gap area + sections 0-17) is a single blob. The gap area coordinates are the beginning of this blob, read by the map geometry loader before it processes the numbered sub-sections.
The HNR road IDs (22-bit values) cannot be linked to FBL road coordinates:
- Not coordinate-derived: Tested 8 hash functions (XOR, CRC32, SDBM, Morton, polynomial, multiply-add, raw truncation, midpoint) — all produce random-level matches.
- Not stored in FBL: Scanning all uint32 values in Monaco FBL finds zero meaningful overlap with HNR entries (3 matches vs 2 expected random).
- Not byte-order dependent: Tested both LE and BE interpretations.
- Opaque identifiers: The IDs are assigned by the NNG map compiler during OSM-to-NNG conversion. They exist only in the compiler's internal mapping.
The navigation engine links HNR to FBL at runtime by loading both files and building an internal lookup table. Without emulating the full map loading pipeline (NngineStart → NngineAttachConfig → file loading), the mapping cannot be reconstructed.
Practical implication: The HNR type A/B classification (major/minor roads) is the best road classification available without DLL runtime emulation.
Confirmed via Unicorn emulation of FUN_101e4560:
FBL key = (tile_index << 23) | sequential_counter
tile_index(9 bits): geographic tile identifier, passed as function parametersequential_counter(23 bits): per-tile sequential ID, stored in object at offset +0xC- The function binary-searches a sorted array of 12-byte entries using this key
This is the FBL's internal spatial index format. The HNR uses a different 32-bit ID scheme (uniformly distributed, not tile-based). The two ID spaces are linked only at runtime through the navigation engine's internal data structures.
The Vatican "road_type" values (0x95, 0x9A, 0xA5) in the inline metadata were
intermediate findings. The actual road class extraction uses value 92 (backslash)
in the varint stream followed by a letter code looked up in DAT_102e3480.
See "Road Class Extraction — SOLVED" section below for the complete solution.
Road attributes are embedded within large opcode records (32-160 bytes each). The FBL sections use an opcode-based record format, NOT flat coordinate bitstreams.
Opcode record types containing road data (from Monaco section 4):
| Opcode | Size | Count | Likely content |
|---|---|---|---|
| 0xD4 | 128B | 13 | Full road segment (coords + attributes) |
| 0xC0 | 123B | 10 | Full road segment |
| 0xA4 | 96B | 13 | Road segment |
| 0xB0 | 94B | 13 | Road segment |
| 0xB4 | 37B | 14 | Segment metadata |
| 0xAC | 34B | 14 | Segment metadata |
| 0xB8 | 35B | 13 | Segment metadata |
| 0xFD | 32B | 20 | Data block |
| 0xE5 | 32B | 18 | Data block |
The road_type/FRC values are packed within these records alongside coordinates and other attributes. Decoding the internal field layout of each record type requires tracing the DLL's record reader functions.
The packed bitstream format uses opcodes (0x00-0xFF) where each opcode
defines the record type and size. The complete opcode→size lookup table was
extracted from nngine.dll at RVA 0x2E58A0.
Correction: The Vatican "road_type" values (0x95, 0x9A, 0xA5) are NOT road classification codes — they are record type opcodes in the bitstream:
- 0x95: 1-byte record (followed by 1 data byte)
- 0x9A: 3-byte record (followed by 3 data bytes)
- 0xA5: 0-byte record (no data follows)
The earlier FRC interpretation (bits 3-5 = road class) was coincidental.
Key opcode ranges:
- 0x00-0x0E: 1-byte records (simple values)
- 0x0F-0x10: 3-byte records
- 0x1D-0x26: 2-byte records
- 0x27-0x29: 4-byte records
- 0x68-0x69: 5-byte records
- 0x6E-0x6F: 33-byte records
- 0x78-0x84: 3-byte records (coordinate-related)
- 0x85-0x86: 5-byte records
- 0x91-0x95: 1-byte records
- 0x96-0x9C: 3-byte records (alternating with 1-byte)
- 0xA4: 96-byte record
- 0xC0: 123-byte record
- 0xD0: 160-byte record (largest)
The opcode table successfully parses ALL FBL sections into structured records:
Vatican section 4 road data (parsed as opcodes):
@138: opcode 0x00, data=0x95 ← road_type value!
@140: opcode 0x00, data=0x05 ← flags
@142: opcode 0x00, data=0x2E ← shape reference (low byte)
@144: opcode 0xB2 (no data) ← shape reference (high byte as opcode)
@145: opcode 0x2B, data=00 14 ← next data
The road_type values (0x95, 0x9A, 0xA5) are stored as data bytes within opcode 0x00 records (1-byte payload). They appear at predictable positions in the record stream — after coordinate records and before shape references.
Section statistics (Vatican):
- Section 1 (curves): 53 records, opcodes 0x00(10), 0x80(6), 0xAC(4)
- Section 4 (roads): 312 records, opcodes 0x00(71), 0x40(12), 0x14(9)
- Section 5: 128 records, large 0xD0(160B) and 0xF9(32B) records
- Section 8: 89 records, mixed opcodes
Monaco section 4: 20,370 bytes parse into structured records with opcodes 0xFD(32B), 0x60(2B), 0x81(3B), 0xE5(32B) — NOT a flat coordinate bitstream.
Correction: The earlier "packed coordinate bitstream" interpretation was wrong. The data is opcode-structured records that happen to produce valid-looking coordinates when misread as N+M bit pairs (because the byte values are uniformly distributed).
Opcodes 0x06 and 0x62-0x67 mark road segment boundaries in the section data.
The DLL's counter function (FUN_1025e228) increments a segment counter at these opcodes.
Segment counts per country (section 4):
| Country | Segments | Section 4 size |
|---|---|---|
| Vatican | 15 | 2 KB |
| Gibraltar | 75 | 20 KB |
| Monaco | 83 | 20 KB |
| San Marino | 251 | 68 KB |
| Andorra | 262 | 77 KB |
| Liechtenstein | 297 | 82 KB |
| Malta | 1,565 | 425 KB |
The segment opcodes (0x62-0x67) are uniformly distributed (~13-16% each) and their payload bytes are also uniform — they do NOT encode road class directly. Road class is encoded within the variable-length record data between segment boundaries.
The segment boundary opcodes (0x62-0x67) are ASCII data type indicators:
| Opcode | ASCII | Type |
|---|---|---|
| 0x62 | 'b' | Boolean |
| 0x63 | 'c' | Char |
| 0x64 | 'd' | Double/Date |
| 0x65 | 'e' | Enum |
| 0x66 | 'f' | Float |
| 0x67 | 'g' | Geographic coordinate |
| 0x06 | — | Generic/untyped |
Each road segment consists of multiple typed fields. The field sequence varies per segment (no fixed schema). Road class is encoded as the value of a specific typed field within the segment's record sequence, but the field position is not fixed — it depends on the segment's attributes.
The variable-length extension mechanism (bytes 0xC0-0xFF at record end) adds
1-5 extra bytes per the second lookup table at DAT_102e5860.
FBL section data uses a UTF-8-like variable-length integer encoding:
| Prefix | Bytes | Value range | Mask |
|---|---|---|---|
| 0x00-0x7F | 1 | 0-127 | 7 bits |
| 0xC0-0xDF | 2 | 128-2,047 | 11 bits |
| 0xE0-0xEF | 3 | 2,048-65,535 | 16 bits |
| 0xF0-0xF7 | 4 | 65,536-2,097,151 | 21 bits |
| 0xF8-0xFB | 5 | 2,097,152-67,108,863 | 26 bits |
| 0xFC-0xFD | 6 | 67,108,864-2,147,483,647 | 31 bits |
Continuation bytes have prefix 0x80 (same as UTF-8).
Corrects the "opcode table" interpretation: The 256-entry table at DAT_102e58a0
is NOT an opcode→size table — it's the varint prefix→byte-count table. The section
data is a stream of variable-length integers, not fixed-size opcode records.
Segment counts (varint-decoded):
| Country | Segments | Values |
|---|---|---|
| Vatican | 81 | 1,517 |
| Monaco | 395 | 14,086 |
| Andorra | 1,440 | 52,955 |
| Malta | 8,096 | 294,053 |
Segment markers are values 6, 98-103 (0x06, 0x62-0x67) in the decoded stream. Road class is encoded within the value sequence but at context-dependent positions.
FUN_102460d0 (graph builder, ~5000 lines)
├── Reads uint32 records from local_68 pointer
├── Dispatches on record type (uVar24 = high 16 bits)
├── Type 0x8003: road class index → reads from local_64+0x2C array
├── Type 0x800E: segment with coordinate data
├── Type 0x8020: junction reference
├── Calls FUN_1024cd70 for sub-record processing
└── Calls FUN_1024d030 for attribute extraction
Input: uint32 record array (NOT raw section bytes)
The raw section bytes must be converted to uint32 records first.
The converter is the preprocessing step we need to find.
Key function: FUN_102460d0 at RVA 0x2460D0 — the main graph builder.
Takes 11 parameters including the uint32 record array and config object.
FUN_1024a720 (RVA 0x24A720) converts raw section bytes to uint32 records:
Input: param_1 = raw section byte pointer
param_2 = flags
param_3 = output record array pointer
param_4 = context object (with lookup tables)
Process:
1. Read byte from param_1
2. If byte > 0xBF: decode as varint (2-5 bytes)
3. Process decoded value to build uint32 record
4. Write record to output array via param_3
5. Advance param_1, repeat
The varint decoding matches UTF-8:
byte & 0x20 == 0: 2-byte (11 bits)
byte & 0x10 == 0: 3-byte (16 bits)
byte & 0x08 == 0: 4-byte (21 bits)
else: 5-byte (26 bits)
This function is the key preprocessing step. It reads the raw FBL section bytes and produces the uint32 record array that FUN_102460d0 processes.
The graph builder processes uint32 records with type in high 16 bits:
| Type | Name | Action |
|---|---|---|
| 0x8000 | end | Terminates processing |
| 0x8001 | skip | Advance past record |
| 0x8002 | set_flag | Sets road flag |
| 0x8003 | road_class | Index 0-9 into road class lookup table |
| 0x8004 | coord_pair | Coordinate pair (lon, lat follow) |
| 0x8005 | advance | Skip 1 uint32 |
| 0x8006 | advance_3 | Skip 3 uint32s |
| 0x8008 | segment | Road segment boundary marker |
| 0x800A | junction_ref | Junction reference |
| 0x800D | end_segment | End of segment data |
| 0x800E | segment_data | Segment with coordinates |
| 0x8019 | return | Return from processing |
| 0x801C | dir_forward | Forward direction |
| 0x801D | dir_reverse | Reverse direction |
| 0x8020 | junction_def | Junction definition |
| 0x8027 | speed | Speed data |
| 0x8028 | lanes | Lane information |
| 0x802A | restriction | Turn restriction |
| 0x8030-0x803B | range | Range/area data |
The graph builder uses type 0x8003 records where the low 16 bits (0-9) index into a 10-entry lookup table at context+0x2C. The table contains OFFSETS into road data, but the INDEX itself IS the road class:
| Index | Road Class | FRC |
|---|---|---|
| 0 | Motorway | 0 |
| 1 | Trunk / Major highway | 1 |
| 2 | Primary road | 2 |
| 3 | Secondary road | 3 |
| 4 | Tertiary / Local connecting | 4 |
| 5 | Local road (high importance) | 5 |
| 6 | Local road (medium importance) | 6 |
| 7 | Local road (low importance) | 7 |
| 8 | Pedestrian / Footway | — |
| 9 | Other / Unclassified | — |
To extract road class from any FBL file:
- Convert raw section bytes to uint32 records (FUN_1024a720)
- Find records with type 0x80030000
- Low 16 bits = road class index (0-9)
In the varint stream, value 92 (0x5C = backslash) marks a road class record.
When the pattern compiler encounters value 92, it calls FUN_10244b70 which
processes the following values and returns a negative road class index.
Verified counts:
| Country | Segments | Value 92 count | Match? |
|---|---|---|---|
| Vatican | 81 | 3 | ✓ (3 known road segments) |
| Monaco | 395 | 70 | ~18% of segments |
| Andorra | 1440 | 191 | ~13% of segments |
| Malta | 8096 | 1148 | ~14% of segments |
Not every segment has a road class marker — only ~14-18% do. The remaining segments likely inherit the road class from a parent or default value.
The road class index is computed by FUN_10244b70 from the values following
the 92 marker. The exact computation requires emulating this sub-function.
Road class is extracted by finding value 92 (0x5C = backslash) in the varint
stream, then looking up the NEXT value in the DLL's lookup table at DAT_102e3480.
Lookup table (key entries):
| Value | Letter | Road Class | Meaning |
|---|---|---|---|
| 65 | A | 1 | Generic/default |
| 71 | G | 2 | Trunk |
| 75 | K | 3 | Primary |
| 66 | B | 4 | Tertiary |
| 98 | b | 5 | Local (high importance) |
| 68 | D | 6 | Local (medium) |
| 100 | d | 7 | Local (low) |
| 83 | S | 8 | Pedestrian |
| 115 | s | 9 | Other |
| 48-57 | 0-9 | 0-9 | Numeric road class |
Extraction results:
| Country | Segments | Classified | Trunk | Primary | Tertiary | Pedestrian |
|---|---|---|---|---|---|---|
| Vatican | 81 | 2 | 0 | 0 | 0 | 0 |
| Monaco | 395 | 31 | 1 | 0 | 1 | 1 |
| Andorra | 1440 | 76 | 2 | 0 | 1 | 2 |
| Malta | 8096 | 424 | 11 | 8 | 11 | 8 |
The HNR routing "weight" is NOT a per-entry continuous value. Statistical analysis confirms:
- Individual bytes are uniformly distributed for both type A and B
- No bit position shows >2% difference between A and B entries
- Popcount is identical (16.03 bits = exactly random)
- 4-byte entries are opaque road segment IDs
The A/B block assignment IS the routing classification:
- Type A blocks = major road segments (used for faster/economic routes)
- Type B blocks = minor road segments
The routing data is binary (major/minor), not a continuous speed or cost value. The 4-byte entries serve only as road segment identifiers for the lookup table.
Exhaustive testing confirms HNR road IDs cannot be linked to FBL segments:
- FBL spatial keys (tile<<23|counter) produce 0 matches in HNR
- MD5 and CRC32 hashes of FBL keys produce 0 matches
- FBL segment counts (13K for 7 countries) vs HNR (19.6M for all Europe)
- Our test files cover only 0.1% of total FBL data
The HNR↔FBL mapping exists only in the DLL's runtime data structures. The map compiler assigns opaque IDs during OSM→NNG conversion. These IDs are not derivable from coordinates, spatial keys, or any hash function.
Practical road classification available without linking:
- FBL road class via value 92 + DLL lookup table (5-14% of segments)
- HNR A/B block split (major 19% vs minor 81%)
- FBL segment size (larger = more important road)
By combining segment count matching (FBL_total × 8.3 ≈ HNR_tile_total) with geographic bbox isolation, 10 countries are definitively linked to HNR tiles:
| Country | FBL segs | HNR tile | Error |
|---|---|---|---|
| FrenchGuiana | 4,403 | 11 | 0.3% |
| Reunion | 14,576 | 21 | 0.4% |
| Guadeloupe | 12,045 | 36 | 0.8% |
| SanMarino | 1,338 | 38 | 0.9% |
| Mayotte | 2,289 | 166 | 1.1% |
| Martinique | 9,553 | 41 | 1.3% |
| Monaco | 395 | 147 | 1.5% |
| Andorra | 1,440 | 51 | 2.0% |
| Liechtenstein | 1,597 | 20 | 3.0% |
| Gibraltar | 426 | 52 | 3.2% |
The matching ratio is 8.3 FBL segments per HNR entry (consistent across all countries). Large countries span multiple HNR tiles and require multi-tile matching.
Economic/Fastest variants:
- Magic:
HNRF, version 351, XOR decryption - Count table at 0x0210: 384 uint32 entries, value >> 8 = record count
- Record size: fixed 256 bytes = 64 entries × 4 bytes
- Entry structure: 4 opaque bytes (road segment ID)
- Block pairs: type A (major roads, ~15%) + type B (minor roads, ~85%)
- Routing weight: BINARY (A/B block assignment), not per-entry
- No temporal patterns (autocorrelation ≈ 0)
- 10 countries linked to tiles by segment count × 8.3 ratio
Shortest variant:
- Same HNRF magic and version
- Count table uses uint16 pairs: (count, record_size)
- Variable record sizes (10K-62K bytes) unlike fixed 256B
- 1,911 total entries across 384 table slots
- Different internal structure from Economic/Fastest
Tile-country linking (verified): FrenchGuiana→11, Liechtenstein→20, Reunion→21, Guadeloupe→36, SanMarino→38, Martinique→41, Andorra→51, Gibraltar→52, Monaco→147, Mayotte→166
The DLL's FUN_1024a720 (RVA 0x24A720) is the decoder that reads raw
section bytes and produces uint32 records. It confirms the UTF-8-like encoding:
Byte range Encoding Value bits
0x00-0x7F 0xxxxxxx 7 bits (0-127)
0xC0-0xDF 110xxxxx 10xxxxxx 11 bits (0-2047)
0xE0-0xEF 1110xxxx 10xx 10xx 16 bits
0xF0-0xF7 11110xxx ... 21 bits
0xF8-0xFB 111110xx ... 26 bits
0xFC-0xFF 1111110x ... 31 bits
The decoder checks bit 19 of its flags parameter (param_2 >> 0x13 & 1).
When set, bytes > 0xBF trigger multi-byte varint decoding. When clear,
each byte is treated as a single value.
The DLL has two tables used for the compiled output format (written by the graph builder, not the raw FBL input):
-
Size table
DAT_102e58a0(164 bytes): maps first byte → token size- Size 0: bytes 0x70, 0x77 (variable-length blocks)
- Size 1: 50 bytes (single-byte opcodes)
- Size 2-6: multi-byte tokens
- Size 33: bytes 0x6E-0x6F (fixed 33-byte records)
-
Extension table
DAT_102e5860(64 bytes): for compiled tokens 0x1D-0x54, if the last byte > 0xBF, adds extra bytes:- 0xC0-0xDF: +1 byte
- 0xE0-0xEF: +2 bytes
- 0xF0-0xF7: +3 bytes
- 0xF8-0xFB: +4 bytes
- 0xFC-0xFF: +5 bytes
-
Character class table at 0x102E50C8 (256 bytes): identity mapping with A-Z → a-z case folding. Used for case-insensitive pattern matching.
Unicorn emulation of FUN_1024a720 on Monaco section 4 (20,370 bytes)
produces 13,451 uint32 records:
- 13,432 data records: raw varint values passed through unchanged
- 19 control records (0x80XX0000 format):
| Type | Count | Meaning |
|---|---|---|
| 0x80090000 | 8 | Separator (divides data into 9 blocks) |
| 0x80300000 | 2 | Road segment type marker |
| 0x80160000 | 2 | Unknown marker |
| 0x80080000 | 2 | Unknown marker (with data field) |
| 0x80170000 | 2 | Unknown marker |
| 0x80180007 | 1 | Attribute (data=7) |
| 0x80010000 | 1 | Section marker |
| 0x80000000 | 1 | End/start marker |
The decoder consumes certain varint values as structural characters:
((0x28, 65 occurrences): opens a group)(0x29, 50 occurrences): closes a group#(0x23, 56 occurrences): reference/hash marker\(0x5C, 70 occurrences): escape sequence (road class, etc.)
These values are consumed by the decoder and either generate control records or modify the decoder's state. They do NOT appear in the output record stream.
Additionally, ~635 other varint values are consumed as arguments to these
structural characters (e.g., the value after \ specifies the escape type).
Large varint values encode packed coordinate pairs:
lon_offset = value >> lat_bits
lat_offset = value & ((1 << lat_bits) - 1)
lon = (bbox_lon_min + lon_offset) / 2^23
lat = (bbox_lat_min + lat_offset) / 2^23
Where lat_bits = ceil(log2(bbox_lat_range + 1)) (21 for Monaco).
Monaco section 4 contains 398 packed coordinate pairs in the main data block (segment 7), matching the expected ~395 road segments.
The non-coordinate data values (0-199 range) have a nearly uniform distribution — each value appears 61-73 times. This suggests the data is a compressed/encoded bitstream split into varint-sized chunks, not discrete attributes.
The 0x80090000 separators divide the record stream into 9 blocks:
| Block | Records | Coords | Description |
|---|---|---|---|
| 0 | 24 | 3 | Preamble |
| 1 | 46 | 3 | Header data |
| 2 | 53 | 4 | Attributes |
| 3 | 105 | 7 | Extended attributes |
| 4 | 68 | 2 | More attributes |
| 5 | 1 | 0 | Separator |
| 6 | 26 | 2 | Pre-road data |
| 7 | 13,079 | 393 | Main road data (97% of records) |
| 8 | 41 | 3 | Post-road data |
Block 7 contains the bulk of the road network data. The graph builder
(FUN_102460d0) processes these records to build the road graph.
The FBL section data is a varint-encoded stream where:
- 7.2% is coordinates (packed lon/lat pairs as large varint values)
- 92.8% is compressed road network structure (junction connectivity, shape points, road attributes, names)
The compressed structure uses a pattern matching grammar implemented
in the DLL's FUN_1024a720. Without the pattern data (stored in the DLL
or derived from the FBL file's structure), we cannot fully decode or
reconstruct the non-coordinate data.
What we CAN do:
- Extract all packed coordinate pairs from any FBL file
- Identify road class markers (value 92 + class code)
- Count segments and identify section boundaries
- Encode new coordinates as packed varint pairs
- Write valid SET containers with XOR encryption
What we CANNOT do yet:
- Reconstruct the 92.8% non-coordinate data from scratch
- Build junction connectivity graphs
- Encode shape points for road curves
- Generate the pattern-matched compressed structure
Next step for full OSM-to-FBL: Extract the DLL's pattern data tables and implement the pattern compiler in Python. This requires deeper emulation of the map loading pipeline.
Previous claim: Large varint values encode packed coordinate pairs
as (lon_off << lat_bits) | lat_off.
Correction: Analysis of the decoded "coordinates" shows ALL longitudes
cluster at 7.4094-7.4095 (the bbox minimum). The value >> lat_bits gives
only 0-1018, not meaningful longitude offsets. These are NOT coordinate pairs.
The large varint values are part of the compressed road network structure. Their interpretation as coordinates was a false positive caused by the lat_offset portion (low 21 bits) happening to fall in the valid latitude range.
Real coordinates are extracted by the packed bitstream approach
(N+M bits per point, as used by fbl_to_geojson.py). The bitstream
coordinates are scattered (not sequential road geometry) because the
bitstream contains interleaved coordinate and non-coordinate data.
The varint stream is a compressed representation where coordinates, attributes, and structure are all encoded together. Extracting individual components requires the full pattern matching grammar.
The previous emulation used an identity character class table which caused incorrect behavior. The REAL table is at RVA 0x2E5408 (char_table_base + 0x340):
Character class values:
0x01 = whitespace (0x09-0x0D, 0x20) — consumed by decoder
0x80 = special chars (NUL, $, (, ), [, \, ^, {, |, ?)
0x1C = digits (0-9)
0x1A = hex letters (a-f, A-F)
0x12 = letters (g-z, G-Z)
0x10 = underscore (_)
0x00 = all other bytes (0x80-0xFF)
Only 6 values (whitespace) have bit 0 set. The decoder's (table[value] & 1)
check only consumes whitespace characters. All other values go through the
pattern matching logic.
With the correct character class table, the decoder produces only 76 records from Monaco section 4 (14,086 varint values). This means:
- 99.5% of varint values are consumed by pattern matching
- 0.5% are output as meaningful data records
- The varint stream IS a compiled pattern language
- The "data" we see is the pattern structure, not raw coordinates
The 76 output records include:
- 73 data values (small integers and a few large values)
- 3 control records: 0x80090000 (separator), 0x80330000 (road type), 0x80000000 (end)
This confirms that the FBL section data is a highly compressed pattern language where the vast majority of bytes encode the pattern structure itself, and only a tiny fraction represents the actual road network data.
Offset Value Meaning
0x00 0x10243AC0 alloc function pointer
0x04 0x10243AD0 free function pointer
0x08 0x00000000 user data
0x0C 0x00000000 (unused)
0x10 0x00000000 (unused)
0x14 0x102E50C8 character table base (identity + case fold)
0x18 0xFFFFFFFF max input length
0x1C 0x00020001 flags
0x20 0x000000FA max records (250)
The character CLASS table is at char_table_base + 0x340 = 0x102E5408.
NngineStart
→ FUN_1000ee00 (create engine object → DAT_103143d0)
→ FUN_101d31b0 (load map file)
→ FUN_101d30a0 (create 0x20-byte map object)
→ FUN_102436e0 (create base object at offset 0x14)
→ FUN_10243790 (create context at offset 0x18, 36 bytes from 0x2E32A4)
→ FUN_10243ae0 (parse section, context = *(obj + 0x18))
→ FUN_1024a720 (varint decoder with pattern matching)
→ FUN_10249210 → FUN_102460d0 (graph builder)
Emulating FUN_1024a720 on all 72 LF-separated lines of Monaco section 4:
| Control Type | Count | Meaning |
|---|---|---|
| 0x80000000 | 52 | End of line/section |
| 0x80010000 | 24 | Sub-section end |
| 0x80330000 | 20 | Road segment group |
| 0x80360000 | 22 | Road segment type 36 |
| 0x80090000 | 21 | Separator |
| 0x80080000 | 20 | Junction marker |
| 0x80170000 | 23 | Marker (unknown) |
| 0x80160000 | 16 | Marker (unknown) |
| 0x800A0000 | 14 | Road attribute |
| 0x80300000 | 10 | Road start |
| 0x800D0000 | 7 | Marker |
| 0x80190000 | 5 | Return to caller |
| 0x80180000 | 3 | Segment type (with data) |
| 0x80030000 | 2 | Road class (data=2=trunk, data=8=pedestrian) |
| 0x801F0000 | 2 | Marker |
| 0x80320000 | 1 | Road type 32 |
| 0x80370000 | 1 | Road type 37 |
Data records: 6,136 values with structured distribution (33-41 per common value). 152 segment marker values (98-103) appear in the data stream. Road class value 92 is consumed by pattern matching and generates 0x80030000 control records.
Line structure: Each LF-separated line is decoded independently. Average 25 data records between control records.
Built using Unicorn emulation of the DLL's FUN_1024a720 regex engine. Decodes FBL section data into uint32 records with 100% accuracy.
Validated on all 7 test files:
| File | Records | Control | Data |
|---|---|---|---|
| Vatican | 518 | 27 | 491 |
| Monaco | 6,379 | 243 | 6,136 |
| Gibraltar | 5,317 | 211 | 5,106 |
| SanMarino | 20,039 | 815 | 19,224 |
| Andorra | 23,158 | 941 | 22,217 |
| Liechtenstein | 27,568 | 1,068 | 26,500 |
| Malta | 131,921 | 4,812 | 127,109 |
The FBL format is a regex-like pattern language where:
- Section data is LF-separated, each line decoded independently
- Metacharacters (
^ $ | [ ] ( ) \ * + ? . {) generate control records - Other varint values are stored as literal data records
- The DLL's decoder (FUN_1024a720) is a full regex engine (1808 lines)
The encoder maps control records back to regex metacharacters:
| Control Record | Metachar | Hex | Description |
|---|---|---|---|
| 0x80090000 | ^ |
0x5E | Separator/anchor |
| 0x80160000 | $ |
0x24 | Line end |
| 0x80010000 | | |
0x7C | Alternation |
| 0x80330000 | + |
0x2B | Road segment 33 |
| 0x80170000 | . |
0x2E | Wildcard/marker |
| 0x800A0000 | [ |
0x5B | Character class/attribute |
| 0x80180000 | \ + data |
0x5C | Escape sequence |
| 0x80000000 | LF | 0x0A | End of line |
Data records are encoded as UTF-8-like varints (pass-through).
Roundtrip accuracy: 100% for data records. Control records round-trip correctly for the simple metacharacter types. Complex control records (0x80360000, 0x80080000, etc.) are generated by multi-varint pattern sequences that require exact byte reproduction.
| Tool | Description |
|---|---|
| decrypt_fbl | XOR decryption |
| fbl_info | Metadata extraction |
| fbl_to_geojson | Coordinate extraction |
| fbl_road_class | Road class extraction |
| fbl_segments | Segment boundary extraction |
| fbl_road_network | Complete road network export |
| fbl_validate | OSM cross-validation |
| fbl_parse | Packed coordinate extraction |
| nng_decoder | Section decoder (Unicorn-based) |
| fbl_replace_section | Template-based section replacement |
| spc_to_csv | Speed camera extraction |
| poi_to_geojson | POI extraction |
| junctions_to_geojson | Junction extraction |
| segments_to_csv | Segment metadata |
| curves_to_geojson | Curve point extraction |
| map_overview | Multi-country overview |
| lyc_decrypt | License decryption |
| hnr_info | HNR routing data |
| hnr_fbl_link | HNR-FBL country linking |
| nng_varint | Varint decoder library |
| nng_emulator | Unicorn emulation framework |
| analyse_header | Header analysis |
| try_lyc_key | License key testing |
| voice README | Voice file documentation |