clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / docs / format.md
format.md 31.7K rendered
1# The `.atlas` format — version 323**Status: normative.** This document specifies the `.atlas` container as it is4frozen at format version 3, semantic conventions version 2. It is written to5be sufficient on its own: an implementer who has never seen Atlas the6application should be able to write a conforming reader from this document and7nothing else. Where this document and any implementation disagree, take it as8a defect in one of them and say so.910The reference implementation is `format/bundle` and `format/semconv`, which11depend on the Go standard library alone. The attribute vocabulary has its own12normative document at [`semconv/REGISTRY.md`](semconv/REGISTRY.md).1314---1516## 1. Vocabulary1718Five words carry the format. They are used precisely throughout.1920| Word | Meaning |21|---|---|22| **Volume** | The subject of one `.atlas` file: a game, a planet, a city. One file, one volume. |23| **World** | A distinct ground within a volume that a reader can stand on. For a game these are its maps; for a captured city, its dated revisions. |24| **Lens** | One raster tile pyramid picturing a world. Worlds are grounds; lenses are pictures of them. A world may have several lenses — map styles, dated captures, warped variants. |25| **Collection** | An ordered group of features within a world. Every collection declares one **kind**: `point`, `path`, or `area`. |26| **Feature** | One point, path, or area, with a title and attributes. Every feature is exactly one kind. |2728A **build** is one packing of a volume's data. Builds of one volume share a29slug and are told apart by their **stamp**. Nothing else in the format is a30noun.3132The format carries no trace of where a volume's data came from. A producer33flattens whatever its sources look like into this shape; a reader only ever34sees this shape. The one exception is the optional merge ledger (§6.6), which35is provenance a reader may show and must never depend on.3637---3839## 2. The three invariants4041These hold for every conforming producer and reader. Everything else in this42document serves them.4344### 2.1 Determinism4546The same input archive, built anywhere, at any time, on any machine, produces47the same stamp and therefore the same file name.4849Concretely: the stamp is an order-independent SHA-256 over named part hashes50(§8.1); the file name is `<slug>-<YYYYMMDD>-<stamp12>.atlas` (§8.2);51`createdAt` is the **newest capture time** across the volume's worlds, never52the build time; and the build ordering is `createdAt`, then policy `revision`,53then `stamp` (§8.3).5455### 2.2 Offline purity5657**No runtime URL appears anywhere in a payload.** A bundle serves with zero58network access, forever. A producer must reject a build whose59`worlds/<slug>.json` or `worlds/<slug>.text` contains the substring `http://`60or `https://` (§9.2). Cross-references that a source published as URLs are61resolved at build time to in-volume identifiers, or dropped.6263### 2.3 Asymmetric conventions6465Producers are strict; readers are lenient — with exactly one exception.6667- **Producers**: an `atlas.*` attribute the registry does not know, or one68 attached to the wrong entity, or one carrying a value outside its69 vocabulary, **fails the build**.70- **Readers**: unknown attribute keys are **ignored, never refused**. A bundle71 written by a newer pipeline still opens in an older reader.72- **The exception**: an unknown `formatVersion` is **refused outright**. A73 reader that guesses at a layout it does not know is worse than one that says74 so.7576---7778## 3. The container7980A `.atlas` file is a **zip archive**. It holds:8182```83atlas.json the manifest — deflated84worlds/<world-slug>.json world payload — deflated85worlds/<world-slug>.bin packed point locations — STORED86worlds/<world-slug>.text per-feature prose — deflated87tiles/<pyramid>/<z>/<x>/<y>.<ext> raster tiles — STORED88icons/<name> icon assets — deflated89```9091### 3.1 Compression policy9293**Tiles and packed locations are stored uncompressed.** They are already dense94— JPEG, PNG, WebP, packed binary — and a stored zip entry can be served as a95straight byte range out of the archive, where a deflated one must be inflated96on every read. This is the one performance property the container is designed97around, and a conforming producer must honour it.9899JSON payloads and icons are deflated: text shrinks severalfold and is read100whole.101102### 3.2 Entry ordering103104`atlas.json` is written first, so a listing of the archive leads with what the105file is. A reader must find entries by name and must not depend on order for106anything else.107108### 3.3 Entry names109110Entry names are slash-separated relative paths of plain segments. A name must111not be empty, must not begin or end with `/`, and no segment may be empty,112`.`, or `..`. A reader that unpacks a bundle to disk is entitled to assume113this held, and a producer must enforce it.114115### 3.4 Slugs116117Volume and world slugs are identifiers used unescaped as both path segments118and URL segments. A slug matches `[a-z0-9_-]+` and must not begin with `-` or119`_`. Nothing else is admitted: everything a hostile name could smuggle needs120more.121122---123124## 4. `atlas.json` — the manifest125126The manifest is what a bundle says about itself. It is read through to list a127volume without touching any payload.128129**A conforming reader must refuse an archive with no `atlas.json`.** The130reference implementation also refuses one whose uncompressed size exceeds 1131MiB, on the grounds that a manifest lists worlds and nothing per feature.132133### 4.1 Schema134135```json136{137 "format": "atlas-bundle",138 "formatVersion": 3,139 "conventions": 2,140 "volume": { "slug": "bend-or", "title": "Bend, Oregon" },141 "version": {142 "stamp": "ec3fe8c21cfec928d43a616eca97ebda08dc0ad7a970f531c677241bb5c70788",143 "createdAt": "2026-08-01T20:13:08.883648Z",144 "revision": 9145 },146 "tileGrid": { "sourceZoom": 13, "firstTile": 4064, "tileSize": 256, "size": 8192 },147 "worlds": [148 {149 "slug": "2026-08-01",150 "title": "2026-08-01",151 "center": { "lat": 0, "lng": 0 },152 "points": 45, "paths": 5, "areas": 16,153 "updatedAt": "2026-08-01T20:13:08.883648Z"154 }155 ]156}157```158159| Field | Type | Required | Meaning |160|---|---|---|---|161| `format` | string | yes | Always `"atlas-bundle"`. A reader refuses any other value, so a stray zip renamed to `.atlas` fails by name rather than by whatever breaks first. |162| `formatVersion` | integer | yes | `3`. A reader refuses versions it does not know (§2.3). |163| `conventions` | integer | omitted when 0 | The semantic-conventions vocabulary the payloads were written against. `2` is current; absent means a bundle from before the conventions existed. A **declaration**, never a gate: a reader that knows a different vocabulary still opens the bundle. Validation is stricter about a bundle that declares (§9.4). |164| `volume.slug` | slug | yes | The volume's identity. Two bundles naming the same slug are two builds of the same volume, however their files are named. |165| `volume.title` | string | yes, non-empty | Display name. |166| `version.stamp` | hex string | yes, non-empty | Content fingerprint (§8.1). Two bundles with equal stamps hold the same data. |167| `version.createdAt` | RFC 3339 | yes, non-empty | The newest capture time across the volume's worlds. **Never the build time.** |168| `version.revision` | integer | omitted when 0 | Policy revision: which generation of build rules produced this file. Orders builds of one capture (§8.3). |169| `tileGrid` | object | yes | The window this volume's worlds are cut from (§4.2). |170| `worlds` | array | yes, non-empty | One entry per world (§4.3). Order is the offering order. |171172### 4.2 `tileGrid`173174| Field | Type | Meaning |175|---|---|---|176| `sourceZoom` | integer | The zoom level the source raster was captured at. |177| `firstTile` | integer | The tile column/row the captured window begins at, in the source grid. |178| `tileSize` | integer | Pixels per tile edge. `256` throughout. |179| `size` | integer | The world square's edge in world pixels. `8192` throughout. |180181A world cut from a window of its own overrides `sourceZoom` and `firstTile` in182its own payload (§6.2).183184### 4.3 `worlds[]`185186| Field | Type | Required | Meaning |187|---|---|---|---|188| `slug` | slug | yes | Names the world's three payload entries and is the world's identity everywhere. Must be unique within the manifest. |189| `title` | string | yes, non-empty | Display name. |190| `parent` | slug | omitted when empty | The world this one was split out of, so an inset sorts with the sheet it came from. |191| `iconOutset` | string | omitted when empty | `light` or `dark`: the rim markers wear to stay legible against this world's art. Duplicated as `atlas.icon.outset` in the payload. |192| `center` | `{lat, lng}` | yes | Opening view, in the volume's own projection. |193| `points`, `paths`, `areas` | integer | yes | Feature counts by kind. Every feature is exactly one of the three, so a listing can say how much a world holds without opening it — and validation holds the payload to the promise (§9.3). |194| `updatedAt` | RFC 3339 | yes | This world's capture time. The manifest's `createdAt` is the newest of these. |195196### 4.4 Manifest encoding is normative197198The manifest's **encoded bytes** feed the stamp (§8.1), so its encoding is part199of the format, not an implementation detail:200201- Field order is exactly as tabled above.202- `conventions` and `version.revision` are omitted when zero. **Every other203 field is always present**, including zero-valued ones (`"points":0`,204 `"center":{"lat":0,"lng":0}`, `"updatedAt":""`).205- Compact — no indentation, no trailing newline.206- HTML-sensitive characters in strings are escaped to their `\u` forms:207 `<` becomes `\u003c`, `>` becomes `\u003e`, `&` becomes `\u0026`. This is208 Go's `encoding/json` default and it is load-bearing — a volume titled209 `Hospitals & Care` is encoded with `\u0026` in every published bundle, and210 an encoder that emits a bare `&` computes a different stamp.211212An implementation that re-encodes a manifest and gets different bytes will213compute different stamps and produce differently named files.214215---216217## 5. World payload entries218219Each world splits three ways, by when its parts are needed:220221| Entry | Read when | Holds |222|---|---|---|223| `worlds/<slug>.json` | the world opens | lenses, collections, inline shape geometry, world attributes, merge ledger |224| `worlds/<slug>.bin` | the world opens | every point feature, packed |225| `worlds/<slug>.text` | a feature's card opens | descriptions, links, per-feature attributes |226227The split exists because descriptions were half the payload by weight and are228read one feature at a time, while point features are overwhelmingly numbers,229and numbers written as text cost several times what they measure. Shape230features are mostly geometry either way, so they ride the JSON inline.231232---233234## 6. `worlds/<slug>.json` — the world payload235236```json237{238 "grid": { "sourceZoom": 5, "firstTile": 0 },239 "lenses": [ … ],240 "collections": [ … ],241 "attrs": { "atlas.geometry.surface": "sphere", … },242 "merged": [ … ]243}244```245246| Field | Required | Meaning |247|---|---|---|248| `grid` | optional | Window override (§6.2). Present only on a world cut from a window that is not the manifest's. |249| `lenses` | yes, non-empty | The pictures of this world (§6.3). |250| `collections` | yes | **Order-significant** (§6.4). May be empty. |251| `attrs` | optional | The world speaking the shared conventions, entity `world`. |252| `merged` | optional | Provenance ledger (§6.6). A reader may show it and must never depend on it. |253254### 6.1 Attributes generally255256`attrs` objects appear on the world, on each collection, and on each feature.257They are flat `string → string` maps. Keys in the `atlas.` namespace are258governed by [`semconv/REGISTRY.md`](semconv/REGISTRY.md); keys outside it are259not the registry's business and pass through. **A reader ignores any key it260does not know** (§2.3).261262### 6.2 `grid`263264```json265{ "sourceZoom": 5, "firstTile": 0 }266```267268Overrides the manifest's `tileGrid.sourceZoom` and `tileGrid.firstTile` for269this world. `tileSize` and `size` are never overridden.270271### 6.3 `lenses[]`272273One raster pyramid picturing this world.274275| Field | Type | Required | Meaning |276|---|---|---|---|277| `name` | string | yes | Display name of the picture, e.g. `"Viking MDIM 2.1"`, `"Basemap"`. |278| `tiles` | string | yes | The pyramid's directory under `tiles/`. Several lenses may name one pyramid. |279| `minZoom` | integer | yes | Lowest zoom the pyramid holds. **`0` in every published bundle.** |280| `maxZoom` | integer | yes | Highest zoom the pyramid holds. |281| `fullZoom` | integer | yes | The highest zoom at which the pyramid is *complete*. Above it, tiles exist only where the capture reached, and `coverage` says where. |282| `sourceZoom` | integer | yes | The zoom this pyramid's source raster was captured at. |283| `formats` | array of string | yes | One file extension per level, indexed by `z − minZoom`: `formats[z − minZoom]` is the extension of every tile at zoom `z`. Length must equal `maxZoom − minZoom + 1`. |284| `bounds` | `{x,y,width,height}` | optional | The window of world pixels the pyramid's tiles cover. |285| `surface` | `{x,y,width,height}` | optional | The ground the world actually covers, where `bounds` is the window cut to draw it. On a piece of a split sheet the window is grown to take in a title drawn beside it, so anything dividing the world up measures `surface` and leaves no cell on margin. |286| `interpolate` | boolean | yes | `true` to resample smoothly (photographic rasters); `false` for nearest-neighbour (pixel art). |287| `background` | string | optional | CSS colour behind the raster, e.g. `"#14181d"`. |288| `shard` | integer | omitted when 0 | Which layer of a split world this lens draws. Features carry a matching `shard`. |289| `coverage` | object | optional | Sparse-level bitsets (§6.3.1). Absent for a level means that level is fully covered. |290291#### 6.3.1 `coverage`292293```json294"coverage": { "3": { "x": 1, "y": 0, "w": 7, "h": 8, "bits": "YDDc//v5cA==" } }295```296297Keyed by **zoom level as a decimal string**. Each value describes which tiles298of that level were actually written:299300- `x`, `y`, `w`, `h` — the bounding tile window at that level.301- `bits` — **standard base64** of a row-major bitset over that window, **least302 significant bit first within each byte**.303304To test whether tile `(z, x, y)` exists:305306```307c = x − coverage.x; r = y − coverage.y308if c < 0 or r < 0 or c >= coverage.w or r >= coverage.h: absent309i = r * coverage.w + c310present ⟺ (bits[i >> 3] & (1 << (i & 7))) != 0311```312313A level with no `coverage` entry is fully covered. A reader must consult the314bitset before requesting a tile: asking for one that was never written is a315wasted request, and the correct fallback is to draw the parent tile.316317### 6.4 `collections[]` — order is significant318319The collections array is the world's whole structure in one flat list.320**Its order is load-bearing**: the packed payload's `owner` column indexes it321(§7).322323Point collections are written first and shape collections after, so a packed324location's `owner` is simply the ordinal of its collection among the points.325The array order is also the legend order.326327| Field | Type | Required | Meaning |328|---|---|---|---|329| `id` | integer | yes | Stable identifier, unique within the world. |330| `title` | string | yes | Display name. |331| `group` | string | optional | A heading several collections sit under. |332| `kind` | string | yes | `point`, `path`, or `area`. |333| `icon` | string | optional | The collection's icon key, before resolution. |334| `iconAsset` | string | optional | The resolved icon's name under `icons/`. Must exist in the archive. |335| `iconPicture` | boolean | omitted when false | The asset is a picture drawn as-is rather than a glyph the reader tints. |336| `color`, `iconColor` | string | optional | CSS colours. |337| `visible` | boolean | yes | Whether the collection is drawn when the world opens. |338| `attrs` | object | optional | Conventions, entity `collection`. |339| `features` | array | optional | Inline features. **A point collection carries none** — its members ride the packed payload. Path and area collections carry theirs here. |340341### 6.5 `features[]` — inline shape features342343Present only on `path` and `area` collections.344345| Field | Type | Required | Meaning |346|---|---|---|---|347| `id` | integer | yes | Unique within the world, across *all* kinds — one world never keys two things alike. |348| `title` | string | yes | Display name. |349| `subtitle` | string | optional | Secondary line. |350| `hasText` | boolean | omitted when false | This feature has an entry in `worlds/<slug>.text`. |351| `parent` | integer | optional | The id of a containing feature. |352| `center` | `{lat,lng}` | optional | A representative point, for framing and labelling. |353| `shard` | integer | omitted when 0 | Which layer of a split world this feature belongs to. |354| `geometry` | array | yes | GeoJSON-shaped geometry objects: `{"type": …, "coordinates": …}`. |355| `attrs` | object | optional | Conventions, entity `feature`. |356357Geometry types must match the collection's kind:358359| Kind | Admitted geometry types |360|---|---|361| `area` | `Polygon`, `MultiPolygon` |362| `path` | `LineString`, `MultiLineString` |363| `point` | *none* — points are never inlined |364365Coordinates are in the volume's own world space (the same `lat`/`lng`366convention as `center`), not WGS 84. A world that declares a spherical surface367also declares the mapping from world space to true coordinates (§6.7).368369### 6.6 `merged[]` — the provenance ledger370371Optional. Present when a build folded more than one source together, or to372record where a single-source world came from.373374```json375[{ "source": "ArcGIS Open Data", "slug": "arcgis-hub", "origin": true,376 "donorFeatures": {"point": 45, "path": 5, "area": 16}, "added": 0 }]377```378379| Field | Meaning |380|---|---|381| `source` | Display name of the contributing source. |382| `slug` | The source's canonical name, so ledgers and tooling agree without translation. |383| `origin` | This entry accounts for the source the world itself came from. Its `donorFeatures` is the world's own tally at composition. |384| `donorFeatures` | `{point, path, area}` — the donor's whole offering, counted per kind. |385| `matched`, `added`, `addedShapes`, `adopted`, `held`, `rejected`, `enrichedCategories`, `alignment` | The merge's own account: what met an existing feature, what was new, what was held back and why. Producer-defined; a reader displays what it recognises. |386387**A reader must never depend on the ledger.** It is provenance a curious388person reads, not data the picture needs.389390### 6.7 Spherical worlds391392A world whose raster pictures a sphere declares so in `attrs`:393394```json395"attrs": {396 "atlas.geometry.surface": "sphere",397 "atlas.geometry.projection": "equirect",398 "atlas.geometry.equirect.px": "0,0,8192,4096",399 "atlas.geometry.equirect.deg": "-180,90,180,-90",400 "atlas.geometry.body": "mars",401 "atlas.geometry.radius_km": "3389.5"402}403```404405`equirect.px` is `x,y,w,h` in world pixels; `equirect.deg` is406`west,north,east,south` in degrees. The mapping is linear in both axes:407408```409x = X + (lon − West) / (East − West) * W410y = Y + (North − lat) / (North − South) * H411412lon = West + (x − X) / W * (East − West)413lat = North − (y − Y) / H * (North − South)414```415416West may exceed east numerically (a world centred on the antimeridian); the417span still reads west to east. `w` and `h` must be positive, and north must418differ from south and west from east.419420A world that says nothing about its surface is a **plane**. A world declaring421`sphere` **must** also declare `projection` and both `equirect.*` keys (§9.4).422423The `atlas.geometry.mercator.*` keys declare a Web-Mercator cut instead: `y`424is linear in the projected latitude, `asinh(tan lat)`, rather than in degrees.425They are experimental.426427---428429## 7. `worlds/<slug>.bin` — `ATLASLOC` version 3430431Every point feature of a world, packed as parallel little-endian typed arrays.432The layout exists so a reader can build typed-array views directly over the433downloaded buffer, with no parsing and no per-feature allocation.434435**All integers and floats are little-endian.** Let `N` be the location count.436437| Offset | Size | Type | Column |438|---|---|---|---|439| `0` | 8 | bytes | Magic: ASCII `ATLASLOC` |440| `8` | 2 | `uint16` | Version. `3`. |441| `10` | 4 | `uint32` | `N`, the location count |442| `14` | 2 | — | Reserved, zero. Present so every column that follows is four-byte aligned. |443| `16` | `4N` | `int32` | **id** |444| `16 + 4N` | `4N` | `float32` | **lat** |445| `16 + 8N` | `4N` | `float32` | **lng** |446| `16 + 12N` | `4N` | `int32` | **member** |447| `16 + 16N` | `4N` | `int32` | **shard** |448| `16 + 20N` | `4(N+1)` | `uint32` | **title offsets** |449| `20 + 24N` | `2N` | `uint16` | **owner** |450| `20 + 26N` | variable | bytes | **title bytes** |451452Total size is `20 + 26N + Σ len(title_i)`.453454### 7.1 Columns455456| Column | Meaning |457|---|---|458| **id** | The feature's identifier, unique within the world across all kinds. Signed 32-bit on the wire. |459| **lat**, **lng** | Position in the volume's world space. Single precision, which resolves far finer than a world is drawn. A conforming reader must expect coordinates to be float32-rounded. |460| **member** | The id of the **area feature containing this point**, or `0` for none. |461| **shard** | Which layer of a split world the point belongs to, or `0` when the world is whole. |462| **owner** | **The index of this feature's collection in the world payload's `collections` array.** This is the only collection identity a reader needs, and it is why that array's order is significant. `owner` must index an existing collection whose `kind` is `point`. |463| **title** | See below. |464465### 7.2 Titles466467The title offsets column holds `N+1` byte offsets into the title region,468relative to its start. Location `i`'s title is469`titles[offsets[i] : offsets[i+1]]`, as **UTF-8, not NUL-terminated**. Offsets470are non-decreasing; `offsets[0]` is `0` and `offsets[N]` is the length of the471title region. An empty title is a zero-length run, which is legal.472473A conforming reader should verify that the offsets are non-decreasing and that474`offsets[N]` does not exceed the remaining bytes, because these are the only475values in the payload that can point outside it.476477### 7.3 Version history478479- **1** — the original packing.480- **2** — added the `shard` column. A world split into layers offers one at a481 time; without it every layer's locations drew over every other.482- **3** — moved meaning without moving a byte. `owner` now indexes the world483 payload's `collections` array rather than a flattened category order, and484 the fifth column reads as `member` — the containing area feature — where it485 previously named a region.486487A reader refuses a version it does not know.488489---490491## 8. Stamps, names, and ordering492493### 8.1 The stamp494495A build's stamp is an **order-independent SHA-256 over named part hashes**.4964971. For each part, the producer records a `name` and a `hash`. A part's hash is498 the lowercase hex SHA-256 of its bytes, except for tile pyramids, whose499 hash is the pyramid's own derivation stamp (which already names its source500 tiles and the tool that derived them, so tiles weigh into the stamp without501 being read).5022. Each part becomes the line `name + " " + hash`.5033. The lines are **sorted lexically** and joined with `"\n"`.5044. The stamp is the lowercase hex SHA-256 of that string.505506The parts a producer records are:507508| Part name | Hash of |509|---|---|510| `worlds/<slug>.json` | the encoded world payload |511| `worlds/<slug>.bin` | the packed locations |512| `worlds/<slug>.text` | the encoded text payload |513| `tiles/<pyramid>` | the pyramid's derivation stamp |514| `icons/<name>` | the icon's bytes |515| `atlas.json` | the encoded manifest **with `version.stamp` and `version.createdAt` both empty** |516517That last row is the one non-obvious rule: the manifest cannot hash itself, so518the stamp is computed over the manifest as it stands *before* the stamp and519the capture time are filled in. `version.revision` **is** included, which is520what makes a policy bump a new stamp.521522> **Constraint.** A part name must contain no space and no newline. Those are523> the record separators of the summed form, so a name carrying either makes524> the sum ambiguous — `"a b"` with hash `"c"` sums identically to `"a"` with525> hash `"b c"`. Archive entry names have never held one. Widening the526> separator would restamp every bundle in existence, so the constraint is kept527> rather than coded around.528529### 8.2 The file name530531A bundle installed in a registry directory is named:532533```534<volume-slug>-<YYYYMMDD>-<stamp12>.atlas535```536537- `YYYYMMDD` is the **first eight digits of `version.createdAt`**, taken by538 counting digits rather than parsing a time, so a name can be derived from a539 manifest that has not been validated. A `createdAt` with no digits yields no540 day segment.541- `stamp12` is the first 12 hex characters of the stamp, or the whole stamp if542 it is shorter.543544The name is for people and for cheap existence checks. **It is never used for545ordering** — that is §8.3.546547### 8.3 Build ordering548549When two builds claim the same volume slug, `a` shadows `b` if:5505511. `a.createdAt > b.createdAt` (string comparison — RFC 3339 sorts lexically);552 else5532. `a.revision > b.revision`; else5543. `a.stamp > b.stamp`; else5554. `a`'s locator (file path) sorts after `b`'s.556557The comparison is **total**, so two scans of one library always agree. Step 2558is what lets a rebuild under new policy supersede the builds already in every559library: the data has not moved, but what a producer makes of it has. A build560from before `revision` existed reads as `0` and loses to any revised build.561562---563564## 9. Validation — what a producer must guarantee565566A conforming producer runs all of these before publishing. A reader may run567them on import; it must not run them merely to open a bundle, since they cost568a pass over every payload.569570### 9.1 Manifest571572`format` is `atlas-bundle`; `formatVersion` is `3`; volume slug is valid and573titled; `stamp` and `createdAt` are non-empty; at least one world is listed;574every world slug is valid, titled, and listed once.575576### 9.2 Offline purity577578Neither `worlds/<slug>.json` nor `worlds/<slug>.text` contains `http://` or579`https://`, anywhere, as a raw substring.580581### 9.3 Structure582583- Every listed world has all three payload entries.584- The packed payload's location count equals `worlds[i].points`.585- Inline path features across all path collections total `worlds[i].paths`;586 inline area features total `worlds[i].areas`.587- Collection ids are unique within a world.588- Every collection's `kind` is one of `point`, `path`, `area`.589- A point collection carries no inline features.590- Every inline geometry type fits its collection's kind (§6.5).591- Every path collection declares `atlas.stroke.width_px`.592- `atlas.label.policy` appears only on area collections — only areas curate593 their labels.594- Every `iconAsset` names an entry that exists under `icons/`.595- Every location's `owner` indexes an existing collection whose kind is596 `point`.597- The world has at least one lens; each lens's `formats` has one entry per598 level; each level the lens claims holds at least one tile.599600### 9.4 Conventions — only when declared601602If `conventions ≥ 1`, additionally:603604- Every `atlas.*` key on the world, on every collection, on every inline605 feature, and in every text entry is registered, attached to that entity, and606 carries a value its vocabulary admits.607- A world declaring `atlas.geometry.surface: sphere` also declares608 `atlas.geometry.projection`, and its `equirect.*` pair parses (§6.7).609- A collection declaring `atlas.geometry.kind` declares the same kind its610 `kind` field does.611- A collection declaring `atlas.icon.std` has a non-empty `iconAsset` — a612 standard icon must be resolved by the time the bundle is written.613614A bundle that declares no conventions is held to none. **The strictness615belongs to the claim.**616617---618619## 10. The registry directory620621A registry is a directory of immutable `.atlas` files. New builds land beside622old ones; **nothing ever mutates a published bundle**.623624- **Scan** reads every `*.atlas` in the directory and takes a descriptor —625 slug, title, stamp, createdAt, revision, size, world count — from each626 manifest. A file that cannot be read is skipped, not fatal: one bad download627 should not take a library down with it.628- **Fold** picks the winning build per slug under §8.3. It is a pure function629 of the descriptors: order-independent, filesystem-free, and always the same630 answer for the same library.631- **Install** validates a candidate, copies it to a temporary name in the same632 directory, renames it into place under §8.2, then reopens and revalidates633 the installed copy — the copy is what will serve. A build already present634 under its own name is a no-op. Installing an old file can never overwrite a635 newer build; it just arrives already shadowed.636- The directory is scanned at launch and rescanned on import. It is not637 watched: a file dropped in externally appears at next launch.638639### 10.1 `index.json`640641A registry directory may carry an `index.json` listing every volume and every642build of it, newest first. **It is always derived from the files it sits with,643never the other way around**, so a hand-copied or hand-deleted bundle is644reflected by the next scan rather than contradicted by a stale listing. It is645always safe to delete.646647```json648{"games":[{"slug":"mars","title":"Mars","versions":[649 {"file":"mars-20260801-68e141f26b1a.atlas",650 "stamp":"68e141f2…","createdAt":"2026-08-01T14:48:10.486177Z",651 "revision":9,"size":214003201,"maps":1}]}]}652```653654Volumes sort by title; builds within a volume sort by §8.3; a volume answers655to its newest build's title.656657> The wire keys `games` and `maps` are history showing through. The listing658> predates the volume/world vocabulary and was never renamed, because nothing659> reads it that would benefit and every tool that writes one would have to660> agree at once. They are kept for compatibility and mean *volumes* and661> *worlds*.662663---664665## 11. Format version history666667| Version | What changed |668|---|---|669| **1** | The original container. Manifest spoke of `game`, `maps`, `pinCount`; payloads lived under `maps/`. |670| **2** | Renamed the format's concepts throughout — game→volume, map→world, variant→lens — in the manifest and the payload keys alike. Payloads moved to `worlds/`. |671| **3** | Unified what a world holds. The payload's separate `groups`, `zones`, and `pins` became one order-significant `collections` array of `point`, `path`, and `area` features; the manifest counts each kind instead of counting only pins; `ATLASLOC` moved to version 3 with `owner` indexing that array. |672673Conventions versions move independently: `1` introduced the `atlas.*`674vocabulary; `2` collapsed the entities `zone`, `category`, and `location` into675`collection` and `feature` to match format 3, and renamed676`atlas.category.key` to `atlas.collection.key`.677678**A reader refuses a `formatVersion` it does not know.** A library accumulates679history, so a registry scan is expected to pass over older builds rather than680fail on them.681682---683684## 12. Checklist for a new reader685686In the order you would implement them:6876881. Open the zip. Find `atlas.json`; refuse the archive without one.6892. Parse the manifest. **Refuse any `formatVersion` but 3.** Ignore fields you690 do not recognise.6913. List worlds from `manifest.worlds`. You now have a working volume listing692 without touching a payload.6934. To open a world: read `worlds/<slug>.json`. Keep the `collections` array694 **in order**.6955. Read `worlds/<slug>.bin`. Check the magic and version, then build views696 over the columns at the offsets in §7. Resolve each location's collection697 by `collections[owner]`.6986. Draw from `tiles/<lens.tiles>/<z>/<x>/<y>.<formats[z − minZoom]>`. Consult699 `coverage` before requesting anything; fall back to the parent tile.7007. Fetch `worlds/<slug>.text` lazily and index it by feature id as a string.7018. Read `attrs` through the conventions registry. **Ignore every key you do702 not know.**7039. Assume no network. If you find yourself constructing an outbound URL from704 payload data, the bundle is malformed or you have misread this document.