# Entangle globe basemap: format and rendering specification Version 1. This document is the contract between the generator (`gen_basemap.py`) and every client that draws the location-channel globe. Android and iOS implement it independently; the conformance files in `testdata/` pin the shared behaviour. Sections marked *normative* must be followed exactly. Sections marked *informative* describe the reference look and may be tuned per platform. The data is derived from Natural Earth (public domain). This folder is released under the Unlicense (see `LICENSE`). ## 1. Overview - Six files are bundled with each app: four level-of-detail (LOD) geometry files, one file of boundary lines and one file of populated places (§3). - Geometry is stored as whole rings and polylines, never cut into tiles. Readers split them into chunks of at most 128 edges for culling and lazy decoding (§4.4). - Edges are straight lines in (longitude, latitude), as in the Natural Earth source. Renderers subdivide long edges so they follow that definition on the sphere (§4.5). ## 2. File format (normative) ### 2.1 Conventions - All fixed-width integers are little-endian. - `uvarint`: unsigned LEB128, at most 5 bytes, value at most 2^32 − 1. - `zz`: zigzag mapping of a signed 32-bit integer to a `uvarint`: `(v << 1) ^ (v >> 31)`; decoding is `(u >> 1) ^ -(u & 1)`. - Coordinates are integers in *units*: `x = round_half_even(lon × units_per_degree)`, `y = round_half_even(lat × units_per_degree)`, with `|x| ≤ 180·U` and `|y| ≤ 90·U`. ### 2.2 Header (32 bytes) | Offset | Type | Field | Value | |---|---|---|---| | 0 | 4 bytes | magic | ASCII `EMAP` | | 4 | u16 | version | 1 | | 6 | u8 | kind | 1 = geometry, 2 = places | | 7 | u8 | lod | 0, 1, 2, or 255 when not a LOD file | | 8 | u32 | file_length | total file size in bytes | | 12 | u32 | units_per_degree | U, 1 ≤ U ≤ 10,000,000 | | 16 | u32 | error_microdeg | simplification error versus the finest LOD, micro-degrees | | 20 | u32 | source_tag | Natural Earth version, `0x00MMmmpp` (5.1.2 = `0x00050102`) | | 24 | u16 | section_count | number of section table entries | | 26 | 6 bytes | reserved | zero | ### 2.3 Section table `section_count` entries of 16 bytes start at offset 32: | Offset | Type | Field | |---|---|---| | 0 | u16 | id | | 2 | u16 | flags (bit 0: the section holds rings; other bits zero) | | 4 | u32 | item_count | | 8 | u32 | offset from the start of the file | | 12 | u32 | length in bytes | Section ids: 1 LAND (rings), 2 LAKE (rings), 3 ADMIN0 (lines), 4 ADMIN1 (lines), 16 PLACES, 17 STRINGS, 18 LOCALES. Writers order the table by id and lay sections out in that order without gaps. Readers ignore unknown ids after checking their bounds. ### 2.4 Geometry sections A geometry section is `item_count` features, back to back: ``` uvarint n vertex count: n ≥ 3 for rings, n ≥ 2 for lines u8 flags rings: bit 0 = hole; lines: bits 0–3 = class zz dx0, zz dy0 first vertex minus the previous feature's first vertex (the first feature is relative to (0, 0)) (n − 1) × (zz dx, zz dy) each vertex minus the previous vertex ``` Rings: - Rings are implicitly closed: the last vertex connects to the first, and the first vertex is not repeated. - Exterior rings are counter-clockwise and holes clockwise in the (x, y) plane, so the interior is on the left of the direction of travel. Each ring is drawn independently with the NonZero rule (§4.6); exteriors and holes need no pairing. - An edge is *artificial* when both endpoints have `|x| = 180·U`, or both have `y = −90·U`. Artificial edges close rings along the antimeridian and the south pole. They are filled but never stroked. Line classes: - ADMIN0: 0 international boundary; 1 disputed, indefinite, line of control, indeterminate frontier or unrecognized; 2 lease or overlay limit. - ADMIN1: 0 boundary; 1 unrecognized boundary. ### 2.5 Places sections PLACES holds `item_count` records of 16 bytes, sorted by display priority. The place id is the record index. | Offset | Type | Field | |---|---|---| | 0 | i32 | x (units) | | 4 | i32 | y (units) | | 8 | u32 | name_ref: byte offset of the default name in STRINGS | | 12 | u8 | scalerank (Natural Earth) | | 13 | u8 | min_zoom_x10: Natural Earth MIN_ZOOM × 10 | | 14 | u8 | labelrank (Natural Earth) | | 15 | u8 | flags: bit 0 country capital, bit 1 province capital, bit 2 megacity, bit 3 world city | Priority order is `min_zoom_x10` ascending, then scalerank ascending, labelrank ascending, population descending, x, y and name bytes. STRINGS is a sequence of unique strings, each `uvarint byte_length` followed by UTF-8 bytes. A `name_ref` points at the length prefix. `item_count` is the number of strings. LOCALES: ``` uvarint locale_count locale_count × { uvarint tag_length, ASCII tag BCP-47 style: "tr", "pt", "zh-Hans", "zh-Hant" uvarint entry_count entry_count × { uvarint place_id_delta, uvarint name_ref } } ``` Locales are sorted by tag. Place ids within a locale are strictly increasing: the first delta is relative to 0, later deltas are at least 1. A locale entry exists only when the localized name differs from the default name. ### 2.6 Reader requirements A reader must reject the whole file (fail closed) when: - the magic, version or kind is wrong, the reserved bytes are not zero, or `file_length` differs from the real size; - the section table or any section lies outside the file or overlaps the header or table; - a varint is longer than 5 bytes, exceeds 2^32 − 1 or runs past its section; - a geometry section does not decode to exactly `item_count` features ending exactly at the section end, a feature is shorter than allowed, or any coordinate is out of range; - a place record or string reference points outside its section, PLACES length differs from 16 × `item_count`, a string is not valid UTF-8, or locale place ids are out of range or not strictly increasing. ## 3. Bundled files | File | Kind | LOD | U | Error | Sections | Source and tolerance | |---|---|---|---|---|---|---| | `globe-l0.emap` | geometry | 0 | 10⁴ | 35,000 µ° | LAND, LAKE, ADMIN0 | NE 110m, collinear points removed | | `globe-l1.emap` | geometry | 1 | 10⁴ | 15,000 µ° | LAND, LAKE, ADMIN0 | NE 50m; Douglas–Peucker 0.012° | | `globe-l2.emap` | geometry | 2 | 10⁴ | 3,000 µ° | LAND, LAKE | NE 10m land + minor islands, lakes; 0.003° | | `globe-l3.emap` | geometry | 3 | 10⁴ | 0 | LAND, LAKE | NE 10m, collinear points removed; lakes 0.001° | | `globe-lines.emap` | geometry | 255 | 10⁴ | 0 | ADMIN0, ADMIN1 | NE 10m; admin-0 0.0005°, admin-1 0.004° | | `places.emap` | places | 255 | 10⁵ | 0 | PLACES, STRINGS, LOCALES | NE 10m populated places | Generator rules (informative; the bytes are pinned by `SHA256SUMS`): - Douglas–Peucker measures distance with x scaled by cos(latitude) from a fixed integer table. Rings are anchored at vertex 0 and the vertex farthest from it. - Rings whose bounding box is under twice the tolerance are dropped (except L3 lakes). - The L0 and L1 errors are nominal: Natural Earth's 1:110m and 1:50m generalization relative to the 1:10m data, scaled from the L2 tolerance by the map-scale ratio. They put the 1:110m data on the whole-globe view, as the picker always had. - Line parts of the same class are joined where exactly two ends meet before simplification. Junctions of three or more lines stay fixed. - Localized names are kept for places with MIN_ZOOM ≤ 6.0. The cutoff drops in 0.1 steps if the localized payload exceeds 450 KiB. - Budget gate: total raw size ≤ 5 MiB and zlib-9 size ≤ 4 MiB. ## 4. Rendering ### 4.1 Camera (normative) The view has a center (φ₀, λ₀) in degrees, a globe radius R in pixels and a screen center (cx, cy). With p the unit vector of a point (φ, λ): ``` p = (cos φ cos λ, cos φ sin λ, sin φ) f = (cos φ₀ cos λ₀, cos φ₀ sin λ₀, sin φ₀) forward e = (−sin λ₀, cos λ₀, 0) east n = (−sin φ₀ cos λ₀, −sin φ₀ sin λ₀, cos φ₀) north depth = p·f visible when depth > 0 X = cx + R (p·e) Y = cy − R (p·n) screen y grows downwards ``` Unprojection: `u = (X − cx)/R`, `v = −(Y − cy)/R`; outside the disc when `u² + v² > 1`; otherwise `p = u e + v n + √(1 − u² − v²) f`, `φ = asin(p_z)`, `λ = atan2(p_y, p_x)`. All camera and geometry math uses 64-bit floating point. Values are narrowed to 32 bits only after screen clipping (§4.6). ### 4.2 Zoom and drag (normative) - Base radius `R₀ = 0.44 × min(width, height)`; zoom `z = R / R₀`, with 1 ≤ z ≤ 300,000. The upper bound frames a geohash cell of precision 8 at a third of the screen. - Latitude of the view center is clamped to [−89°, 89°]. - A drag of (dx, dy) pixels changes the center by `Δφ = dy·k` and `Δλ = −dx·k / max(cos φ₀, cos 80°)^w`, where `k = 180 / (π R)` degrees per pixel and `w = smoothstep(3, 12, z)` with `smoothstep(a, b, x) = t²(3 − 2t)`, `t = clamp((x − a)/(b − a), 0, 1)`. At globe scale the spin is unchanged; zoomed in, the map follows the finger at every latitude. ### 4.3 LOD selection (normative) - Degrees per pixel at the view center: `dpp = 180 / (π R)`. - With `εᵢ` the header error of LOD i in degrees and `current` the LOD drawn last frame: refine while `ε_current / dpp > 0.6` (and a finer LOD exists), then coarsen while `ε_(current−1) / dpp < 0.4`. - `globe-lines.emap` is drawn when the selected LOD is at least 2; below that, the ADMIN0 section of `globe-l1.emap` (LOD 1) or `globe-l0.emap` (LOD 0) is drawn. Places are always drawn. - A frame draws a layer at a LOD only when every chunk it needs at that LOD is decoded; otherwise it draws the next coarser LOD that is ready and requests another frame. L0 is decoded completely at load, so it is always ready. The reference renderer decodes at most 96 chunks per frame. ### 4.4 Chunks and culling (normative) - A chunk is up to 128 consecutive edges of one feature. A ring of n vertices has n edges (the last joins vertex n−1 to vertex 0); a line has n−1. Chunk k covers edges 128k … 128k+127 and therefore vertices 128k … 128k+128 (taken mod n for rings), including the first vertex of the next chunk. - Each chunk has a spherical cap (unit center c, angular radius ρ) that contains the lat/lon bounding box of all its vertices. A cap built from the box center and its farthest corner qualifies. Because edges are linear in (lon, lat) they stay inside that box. - Classification against the camera (rect = the clip rectangle: the view enlarged by 64 px on every side, wider than any stroke or glow): - BEHIND when `c·f < −sin ρ`. - OFFSCREEN when `c·f > sin ρ` and the disc of radius `2R sin(ρ/2)` around c's projection lies entirely beyond one edge of the rect. - VISIBLE otherwise. - For fills a BEHIND or OFFSCREEN chunk contributes only its first vertex. This is exact: the replaced path and the shortcut both stay behind the limb or beyond the same rect edge. For strokes such chunks contribute nothing. - A whole feature may be skipped when its own cap is BEHIND or OFFSCREEN. ### 4.5 Edge subdivision (normative) An edge between vertices whose angle is L radians is split into `⌈L / L_max⌉` pieces with `L_max = √(2 / R)`. Intermediate points are interpolated linearly in (lon, lat) from the integer coordinates, then projected. This keeps long straight boundaries (for example along a parallel) within half a pixel of their definition. Bisecting recursively until pieces are at most L_max, and skipping pieces that are culled, draws the same result (the Android renderer does this). ### 4.6 Fills (normative) 1. Project the ring's vertices (after chunk reduction and subdivision). A vertex is front when depth > 0. 2. Ring entirely front: the projected ring is the polygon. 3. Ring entirely behind: if the view center lies inside the ring (planar point-in-polygon in (lon, lat) on the full-resolution ring), the polygon is the whole limb circle; otherwise nothing. 4. Otherwise split the ring into front runs. Where an edge from a to b changes side, the limb point is the projection of `q = a + t(b − a)`, `t = d_a / (d_a − d_b)`, normalized to the unit circle in the (e, n) plane. Its limb angle is `α = atan2(−(q·n), q·e)` in screen coordinates. 5. Sort all limb points by α. From each run's exit point, follow the limb to the next entry point: in decreasing α for exterior rings, increasing α for holes. Arc points are spaced `clamp(2√(0.5 / R), 0.01, 0.04)` radians apart on the circle of radius R. Continue with that entry's run until the loop closes; repeat for unused runs. 6. Clip each loop to the clip rectangle `(−64, −64, w + 64, h + 64)` (w, h = view size) with Sutherland–Hodgman in 64-bit floating point. 7. Decimate: drop a vertex closer than `minVertexSpacingPx` (§4.9) to the last kept one, except limb points, arc points and clip intersections. 8. Append every loop of every ring of a layer to one path and fill it with the NonZero rule. Lakes are filled the same way into a second path that is painted with the ocean's own brush. Islands inside lakes are holes and show the land beneath. ### 4.7 Strokes (normative rules, informative styling) - Coastline: every non-artificial edge of every LAND ring (lake shores use the same style at 60% alpha). Line segments that cross the limb end at the limb point of §4.6 step 4. - Segments are clipped with Liang–Barsky to the rect and decimated like fills. - Reference styling: coastline 1.4 px, admin-0 1.2 px, admin-1 0.8 px. ADMIN0 class 1 at 60% alpha, class 2 at 35%. Admin-1 uses the border color at 70% alpha and fades in between dpp 0.012 (alpha 0) and 0.008 (alpha 1). - Natural Earth 10m is a 1:10,000,000 product. Below dpp 0.0004 the coastline alpha falls log-linearly to 45% at dpp 0.00005, so the line does not suggest precision the data lacks. ### 4.8 Places and labels (informative) - Equivalent web-map zoom: `Z = log₂(2π R_dp / 256)` with `R_dp` the radius in density- independent pixels. Candidates are places with `min_zoom_x10 / 10 ≤ Z + 0.6`, in priority order, front-facing (depth > 0.03) and on screen. - Placement is greedy: label to the right of the dot (left for right-to-left scripts) with 4 dp padding. When that side leaves the screen or overlaps a placed label or an excluded area (the crosshair, the cell labels and any UI drawn over the globe), the other side is tried; if both fail the label is skipped. Labels visible in the previous frame win ties, and a fading label keeps its side. At most 40 labels; text widths are cached per place. - Labels fade in over 180 ms and out over 120 ms. While the globe moves, the motion-detail ladder (§4.9) decides: FULL keeps labels, BALANCED and FAST hide them. - Names use the UI typeface (Geist Sans); cell ids use the monospace typeface (Geist Mono). - Locale tag for a UI locale: language `iw` → `he`, `in` → `id`; `zh` follows its script (Hant → `zh-Hant`, Hans → `zh-Hans`) and, without a script, regions TW, HK and MO → `zh-Hant`, others `zh-Hans`; every other language uses its code alone (`pt-BR` → `pt`). Missing tags or entries fall back to the default name. ### 4.9 Motion-detail ladder (informative) Renderers measure frame time while the globe moves and step between three levels with hysteresis. The ladder never changes the LOD, so nothing pops when motion stops. | | FULL | BALANCED | FAST | |---|---|---|---| | minVertexSpacingPx | 0.75 | 1.5 | 2.5 | | land point stride (rings ≥ 64 vertices) | 1 | 2 | 2 | | admin-1 | on | off | off | | admin-0 | on | on | off | | lakes, coastline | on | on | on | | places | dots + labels | dots of capitals and megacities | none | | grid | halo, neighbours, selection | halo, selection | off | At rest the renderer always draws FULL. The graticule is the same at every level: 15° steps, parallels from −75° to 75°, meridians from pole to pole, 1 px wide. Ladder control: when motion starts the level is BALANCED. The renderer averages the durations of 8 consecutive frames, each clamped to 1–50 ms, and then steps: FULL goes to FAST above 34 ms and to BALANCED above 22 ms; BALANCED goes to FAST above 29 ms and to FULL below 18.5 ms; FAST goes to BALANCED below 22 ms. A non-finite average means FAST. ### 4.10 Cell grid presentation (informative) The grid is drawn through a cell abstraction (cell at a point, outline, neighbours, halo lines, level for a radius, zoom for a level, coverage). The location-channel cells are geohashes today. - Halo: the (2k+1)² block of cells around the selected cell drawn as one stroked path whose alpha falls off radially from the crosshair to zero at 0.6 × the larger screen dimension. Neighbour fills (alpha 0.05) and labels use the same falloff. For each drawn level, k = ⌈half the screen diagonal / side⌉ clamped to 1…4, where side is the cell's smaller span in pixels: min(Δlat, Δlon · cos(mid-latitude)) · R · π/180. The lattice is the 2k + 2 meridians `lon_min + i·Δlon` and parallels `lat_min + j·Δlat` (i, j = −k … k + 1), clipped to ±90°. Halo strokes are 1.6 px in the grid color times the level's alpha. - Level cross-fade: at most three levels are drawn; the newest fades in over 220 ms while the others fade out. Fades keep running while the grid is hidden (fast motion), so it returns smoothly. The first level of a session appears at once. - Selection morph: the old (currently shown) and new outlines have 64 points each: 16 evenly spaced points per edge, counter-clockwise from the south-west corner (so the corners are kept), with longitudes unwrapped around the new cell's center. They are interpolated over 160 ms with the fast-out-slow-in curve. The first selection appears at once. The haptic tick fires immediately on every change. - Selected cell: fill accent at alpha 0.20, glow stroke 7 px at alpha 0.35, stroke 3.2 px. - Cell ids (the monospace typeface) are centred on the cell centre and skipped when its depth is below 0.08. The selected id is SemiBold 12.5 sp in the accent colour with a label-halo stroke of 0.18 × the text size at alpha 200/255. Neighbour ids are Medium 10 sp in the label colour at alpha 160/255 × clamp(depth, 0.4, 1) × falloff, with the halo at 120/255; neighbours whose falloff is 0 are skipped. Place labels avoid the boxes of the drawn ids. ### 4.11 Visual tokens (informative) Colors come from the app theme: accent, land, coastline, border, ocean center and edge, atmosphere, graticule, grid, label, label halo and star. The reference values: | Token | Dark | Light | |---|---|---| | accent, atmosphere | `#D7BA7D` | `#8A6A1F` | | land | `#262421` | `#CFC9BE` | | coastline | accent at 0.45 | accent at 0.5 | | border | `#A3A3A3` at 0.5 | `#5E5A52` at 0.6 | | ocean center / edge | `#100F0E` / `#040404` | `#F2EFE9` / `#DFDBD2` | | graticule | `#E6E6E6` at 0.055 | `#1C1B19` at 0.08 | | grid | `#A3A3A3` at 0.6 | `#5E5A52` at 0.7 | | label | `#A3A3A3` | `#5E5A52` | | label halo, background | `#151515` | `#FAF9F6` | | star | `#E6E6E6` | `#5E5A52` | Stroke widths in this document are device pixels. Strokes use butt caps and miter joins with a miter limit of 4. - Atmosphere: radial gradient to 1.22 R, accent alpha 0.30 at the center, 0.12 at 0.75, clear at the edge. - Ocean: radial gradient centered at (−0.32 R, −0.38 R) from the globe center with radius 1.5 R; ocean center color to 0.7, ocean edge color at 1. - Limb shading: radial gradient centered at (−0.25 R, −0.30 R), radius 1.35 R, clear to 0.62, black 0.34 at 0.88, black 0.72 at 1; then a linear gradient from the upper left to the lower right, clear to 0.55, black 0.42 at the end. - Draw order: stars (static layer), atmosphere, ocean, graticule, land, lakes, coastline, admin-1, admin-0, shading, places, grid, crosshair (own layer). - When R exceeds 10 × the larger view dimension, the view lies inside the flat parts of the ocean and shading gradients, so the ocean is a plain fill and atmosphere and shading are skipped. The result is identical and avoids huge gradient circles. ### 4.12 Interaction (informative) The reference behaviour of the picker; the web client follows it too. - **Viewport:** `R₀ = 0.44 × min(w, h)` (§4.2). The camera centre (cx, cy) is the middle of the view; a client that keeps part of the view for its own UI may move it, keeping R₀ from the full view. Such a client may also let the zoom go below 1, down to where the whole globe fits the area it leaves for the map (the web client does). The selected cell is always the cell at the view centre at the current level. - **Auto level:** after every zoom change the level becomes the largest p (1 … 12) whose cell latitude span `180 / 2^⌊5p/2⌋` degrees covers at least 0.22 × min(w, h) on screen, i.e. `span · π/180 · R ≥ 0.22 · min(w, h)`. - **Zoom for a level:** the zoom that shows the cell's latitude span at a third of min(w, h): `z = (min(w, h) / 3) / (span · π/180) / R₀`, clamped to 1 … 300,000. - **animateTo(lat, lon, zoom?, level?, duration = 550 ms):** FastOutSlowIn easing (`cubic-bezier(0.4, 0, 0.2, 1)`); latitude moves linearly in t, longitude by the shorter way round, zoom exponentially (`z₀ · (z₁ / z₀)^t`). With a level the level is fixed for the whole animation; without one it follows the zoom (auto level). - **Intro:** the view starts at (0.4 × target latitude, target longitude − 70°) with z = 1 and, once the view size is known, runs animateTo(target, zoom for the target level, that level, 1,400 ms). It plays once. - **Press:** any pointer down cancels the running animation and marks the globe as moving. - **Drag:** starts once the pointer has moved the touch slop (8 dp); each move then applies the drag mapping of §4.2 to the movement. - **Pinch:** multiplies the zoom by the change of the finger distance, about the view centre; the fingers' common movement drags. Auto level follows. - **Fling:** only after a single-pointer drag. The release velocity (px/s, from a velocity tracker over the recent single-pointer samples) is clamped per axis to ±3,200; an axis below 90 px/s becomes 0. Each frame, with dt clamped to 1/30 s, the globe is dragged by v·dt per axis, each component clamped to 0.12 R, and v is multiplied by e^(−4.2·dt). It stops when both axes are below 90 px/s. - **Tap:** one pointer, released within 400 ms, moved less than the touch slop, on the globe disc: animateTo(the point under the finger) over 450 ms at the same zoom. A second tap within 350 ms and 4 × the touch slop of the first is a double tap: animateTo(the point, zoom × 1.9) over 500 ms. The first tap is not delayed. - **Level buttons:** − and + change the level by one (1 … 12) and run animateTo(the centre, zoom for that level, that level, 450 ms). - **Haptic tick:** every change of the selected cell after the first fires a light tick (keyboard tap), during animations too. - **Web pointer and wheel (web client):** the mouse wheel and trackpad scroll zoom about the point under the pointer, so that point stays under the pointer. Wheel deltas are normalised to pixels (line mode × 16, page mode × the view height) and applied as `z × 2^(−Δ / 500)`; a trackpad pinch (a wheel event with the control key) uses `z × 2^(−Δ / 100)`. The globe counts as moving until 150 ms after the last wheel event. Arrow keys drag by 1/8 of min(w, h), `+` and `−` act as the level buttons. The web has no haptics. ### 4.13 Overlay (informative) - **Stars:** a static layer behind the globe: 160 stars per phone-sized view (a web client keeps the same density for larger views), uniformly placed, radius 0.6–2.1 dp, alpha 0.15–0.70, in the star colour. Positions come from a seeded generator; they need not match between platforms. - **Crosshair:** its own layer at the camera centre, in the accent colour: four arms 9 dp long starting 5 dp from the centre, 1.6 dp wide, and a centre dot of radius 1.8 dp. At rest its alpha pulses between 0.45 and 1 (1,100 ms each way); while the globe moves it is 0.9. - **Places:** names in the UI typeface, Medium 10 sp, in the label colour at 0.78 × the fade, over a label-halo stroke of 0.16 × the text size at 0.55 × the fade. Dots have radius 2.6 dp for country capitals and megacities, 1.8 dp otherwise; country capitals use the accent colour, others the label colour at 0.85. A dot's alpha is at least 0.35. Labels keep a ±24 dp square around the crosshair clear and sit 1.8 dp (dot radius) + 4 dp from the dot. Fading-out labels follow their place at the offset they had. - **Card:** the selected cell as `#` (monospace, Medium 14 sp) over ` • ~` (10 sp). Level names: 1–2 Region, 3–4 Province, 5 City, 6 Neighborhood, 7 Block, 8 and more Building. Size is the cell width by level: 1 → 5,000 km, 2 → 1,250 km, 3 → 156 km, 4 → 39.1 km, 5 → 4.89 km, 6 → 1.22 km, 7 → 153 m, 8 → 38.2 m, 9 → 4.77 m, 10 → 1.19 m, then ÷4 per level; written `~%.0f km` from 100 km, `~%.1f km` from 1 km, `~%.0f m` below. ## 5. Determinism and verification - `gen_basemap.py` parses inputs as Decimal, quantizes with round-half-even and makes every geometric decision with exact integer arithmetic. `--check` rebuilds and compares every copy; `SHA256SUMS` pins the outputs; `basemap/assets/` is the tracked copy the web site reads. - Inputs are pinned by URL, size and SHA-256 in `inputs.json` and parsed only after the hash matches. ## 6. Conformance files | File | Content | |---|---| | `testdata/fixture-geometry-v1.emap` | synthetic geometry file: rings with a hole, an antimeridian ring, a polar ring, a lake, lines | | `testdata/fixture-places-v1.emap` | synthetic places file with localized names | | `testdata/fixture-v1.json` | the decoded content of both fixtures | | `testdata/vectors/camera.json` | projection and unprojection cases (§4.1) with tolerances | | `testdata/vectors/drag.json` | drag mapping cases (§4.2) | | `testdata/vectors/lod.json` | LOD selection cases (§4.3) | | `testdata/vectors/chunks.json` | chunk vertex ranges (§4.4) | Every client must decode both fixtures to exactly the JSON content and match the vectors within their tolerances. ## 7. Regional detail files (optional; web) A client may add finer data for one region on top of the bundled files. The web client does this for Türkiye with files built by `gen_region.py` from OpenStreetMap; the apps do not bundle them. They use the v1 encoding of §2 with these additions: - New section ids: 5 ADMIN2 (lines: districts), 6 ROADS (lines; class 0 motorway, 1 trunk, 2 primary, 3 secondary), 19 REGION and 20 MASK (rings). Readers of §2 ignore them. - REGION holds one 16-byte record, `i32 west, south, east, north` in units: the region rectangle. - MASK holds the rings of the area the region's boundaries, lakes, roads and places cover (the country with its waters, grown by about 2 km). - `source_tag` is the date of the OpenStreetMap data as the decimal number YYYYMMDD. - Geometry LODs continue the ladder: 4 and 5, with header errors in micro-degrees. For selection (§4.3) inside the region the LOD 3 error counts as 2,000 µ°, so the ladder is 35,000 / 15,000 / 3,000 / 2,000 / R4 / R5 with the same 0.6 / 0.4 hysteresis. The region is used when its rectangle is visible and the ladder selects 4 or more. - Land and coastline come from the region inside the rectangle and from the global LOD outside it. Ring edges whose two ends lie on the same side of the rectangle are artificial (filled, never stroked). The coastline fade of §4.7 does not apply to region data. - Lakes, boundaries, roads and places come from the region inside the mask and from the global files outside it. Reference styling (informative): region admin-0 like global admin-0 class 0; ADMIN1 0.8 px in the border colour at 0.7; ADMIN2 0.6 px at 0.45, fading in between dpp 0.0016 and 0.0008; roads in `#8C8577`, motorway 1.6 px at 0.4, trunk 1.3 px at 0.34, primary 1.0 px at 0.28, secondary 0.8 px at 0.22, each fading in over a factor of two of dpp ending at 0.004, 0.001, 0.0004 and 0.00015. Features whose bounding cap spans less than the decimation spacing are skipped. Files (web/data/tr): `tr-r4.emap` (LOD 4, about 50 m: land, lakes, admin-0/1/2, REGION, MASK), `tr-r5.emap` (LOD 5, about 5 m: land, lakes, REGION), `tr-roads.emap` (ROADS, REGION) and `tr-places.emap` (places in the mask: cities and towns, and İstanbul's suburbs, quarters and neighbourhoods; default names in Turkish, an `en` locale from `name:en`).