2. Data and rebakes¶
What you see¶
Nothing, when it works. The map, the places, the speed limits, the stop signs and the camera dataset all come from files this repository builds and hosts on GitHub releases. They are rebuilt on a schedule, and a phone picks up the new build either by streaming it or by offering you an Update on a region you downloaded, in Settings > Offline maps.
When it does not work, what you see is a region that looks wrong for no visible reason: a state whose downloaded map has no businesses, a country whose house numbers vanished, or an Update button that never appears. Most of this chapter is about why those happened and what now stops them.
Where the data comes from¶
One release per dataset, each with its own manifest the app reads, each independently rebuildable
without shipping an app update. Every one is a fixed-tag prerelease ("infrastructure release")
whose assets exist nowhere else, so any cleanup that deletes releases selects by the tag pattern
v0.* and never by "prerelease" or "old".
| Dataset | Release tag | Manifest | Built from | Used for |
|---|---|---|---|---|
| Open places | places-overlays |
places-overlay-manifest.json |
Overture Places, AllThePlaces, OpenStreetMap | The businesses on the map, offline and online |
| Offline basemap | basemap-tiles |
basemap-manifest.json |
OpenStreetMap via planetiler 0.10.2 | The map itself with no signal |
| World floor | basemap-tiles |
basemap-world.pmtiles (no manifest of its own) |
Natural Earth via planetiler | Coastlines, water, borders and place names anywhere, offline |
| Offline routing | obf-regions |
obf-manifest.json |
OpenStreetMap via OsmAndMapCreator | Turn-by-turn with no signal, and posted speed limits |
| Offline place search | poi-packs |
poi-pack-manifest.json |
OpenStreetMap | Searching places and addresses with no signal |
| Road features | road-features |
road-features-manifest.json |
OpenStreetMap | Traffic lights, stop signs, crossings, speed bumps, speed cameras |
| Surveillance cameras | flock-cameras |
flock-manifest.json |
DeFlock, in OpenStreetMap | The camera layer and the avoid-cameras feature |
| Buildings | building-overlays |
building-overlay-manifest.json |
Microsoft building footprints (ODbL) | Filling OSM's suburban building gaps |
| House numbers | address-overlays |
address-overlay-manifest.json |
OpenAddresses | House numbers where OSM has no addr:housenumber |
| Speed limits | maxspeed-overlays |
maxspeed-overlay-manifest.json |
OpenStreetMap maxspeed |
The posted limit without a routing download |
| Voices and speech | asr-models (speech recognition); Piper voices come from sherpa-onnx's own tts-models release; tts-runtime holds the build-time sherpa-onnx AAR |
Piper catalog in :core |
Piper, sherpa-onnx | On-device speaking and listening |
| Map fonts | map-fonts |
none (one zip) | Roboto over Noto | Label glyphs for the offline style; online they come from GitHub Pages |
Every manifest URL is a build constant with a Gradle override for local testing, so a dev build
can point at a manifest served from the laptop through adb reverse:
-PplacesManifestUrl, -PbasemapManifestUrl, -PworldBasemapUrl, -PobfManifestUrl,
-PpoiPackManifestUrl, -ProadFeaturesManifestUrl, -PflockManifestUrl,
-PoverlayManifestUrl (buildings), -PaddressManifestUrl, -PmaxspeedManifestUrl and
-PmapFontsUrl. Without one, each reads
https://github.com/PimpinPumpkin/Vela/releases/download/<tag>/<file>, except -PmapFontsUrl,
which overrides the online glyph base on GitHub Pages; the offline font zip's URL is fixed in
GlyphPackStore.
The catalogs. Rows come from four files in tools/, and the ids are shared wherever the
extract is shared:
| Catalog | Rows | Used by |
|---|---|---|
routing-regions.json |
458 | obf, basemap, place packs, road features, speed limits; carries each row's Geofabrik pbf_url |
places-regions.json |
448 | the places bake (its own boxes, same ids, the OSM extract looked up by id in the routing catalog) |
overlay-regions.json |
361 (groups us 51, world 185, chunk 125) |
building footprints |
address-regions.json |
52 | house numbers (US states with an OpenAddresses source) |
The routing catalog covers every country-level extract Geofabrik publishes, plus first-level
sub-areas for the countries Geofabrik divides (germany-sub, france-sub and so on, dispatched
together as all-sub). china-sub joined on 2026-09-21: Geofabrik cuts China into 33
sub-extracts (every province plus Beijing, Shanghai, Tianjin, Chongqing, Hong Kong and Macau, the
largest 164 MB), so they bake like germany-sub while the whole-country row keeps
skip_obf: true. Eleven rows carry that flag (California, France, Germany, Great Britain, Italy,
Spain, Japan, India, Indonesia, Brazil, China): they run out of memory in the routing bake even
filtered, and their sub-area rows cover them. 63 rows are big: true (over 450 MB of extract).
How it is decided¶
When each rebake runs¶
One bake at a time, started by the bake conductor (bake-conductor.yml, hourly at :05,
scripts/bake-conductor.py, schedule in tools/bake-schedule.json). The data bakes have no crons
of their own since 2026-09-29: on their own clocks they overlapped, and every bake shares the
repository's 1,000 GitHub API requests an hour with CI and each other, so a heavy night failed the
canary release, the F-Droid index and the bakes themselves with HTTP 403 and mailed a failure per
region. Each hour the conductor:
- settles the run it started last: success marks the job fresh; failed regions are queued for a
retry of exactly those regions (a fresh dispatch with the job's region-list input, at most three
retries a cycle); a run where only the manifest step failed is rerun (
gh run rerun --failed); - starts nothing while any heavy bake is running (any workflow in the schedule) or while fewer than 400 API requests are left this hour;
- otherwise starts ONE bake: a pending retry first, else the most overdue job.
Its own run never fails, so it cannot mail a failure. Its record is state.json on the
bake-conductor release (targeted at the root commit, like the cells releases, so it never sorts
above an app release); the run summary shows every job's last good bake and next due date.
Job (tools/bake-schedule.json) |
Due every |
|---|---|
Open places, one seventh of the catalog (places-slice, the weekday's seventh) |
24 h |
Offline place search, two halves (poi-a, poi-b) |
30 days |
| Road features, two halves | 30 days |
| Offline basemap, two halves | 30 days |
| Grid cells: US, and the rest of the catalog in two sets | 30 days |
Buildings (us, world, chunk), house numbers, speed limits (two halves) |
90 days |
| Surveillance cameras | Weekly cron, Mondays 08:17 (small, not a heavy bake) |
Offline routing (obf-regions, with the highway hierarchy), US and the rest in two sets |
90 days, into the STAGING manifest, which the conductor then publishes as the live obf-manifest.json after a clean cycle |
| World floor | Manual only (world-lowzoom.yml with publish: true) |
Every workflow can still be dispatched by hand from the Actions tab; the conductor sees a
hand-started bake as running and waits for it. Changing a cadence or adding a bake is an edit to
tools/bake-schedule.json, not a new cron.
The monthly places bake always bakes against the newest Overture release in the public bucket
(a dispatch can pin one; the fallback is 2026-08-19.0), and since 2026-09-22 against the newest
AllThePlaces run too (runs/latest.json; the fixed id 2026-09-05-13-32-25 is only the fallback
when that fetch fails). Before that, every bake since 2026-09-15 carried the same week-old chain
locator data while AllThePlaces publishes weekly.
The daily seventh. OpenStreetMap is a real source of places, not just a donor of positions,
and it is the one source anybody can fix. So a seventh of the places catalog rebakes every day:
the catalog is sorted by id, and a row bakes on the day where index % 7 equals the weekday
(Monday = 0, UTC). Every region comes round once a week, always on the same weekday, about 64 regions a
day. A seventh rather than everything because every rebake republishes the archive, and anyone
who downloaded the region is offered it again; streaming users pick it up with no prompt at all.
The quarterly group. The conductor runs the building groups, the house numbers and the two
speed-limit halves as separate jobs, one after another. quarterly-data-refresh is kept for a
manual all-at-once refresh (it fires them together, which is what the conductor exists to avoid).
The obf bake runs into the staging manifest, and the conductor copies staging over the live obf-manifest.json once all three routing jobs finished a clean cycle (every region baked, retries included) and staging passes its checks: no live region missing, no revision going backwards, every file on the release, something newer; the old live manifest is kept as obf-manifest-previous.json. To roll back, copy obf-manifest-previous.json over the live name.
A rebake overwrites the current generation in place: same asset names, same manifest. New generations only fork when a file format changes, which is a deliberate cutover, never a cron.
How a bake job runs¶
Every workflow has the same shape: a plan job builds the matrix from a catalog, one job per region
bakes and uploads only its own archive and emits a manifest entry as a build artifact, and a final
job publishes the manifest. Parallelism per workflow: places max-parallel: 8 per shard, basemap 4,
obf 12, place packs 12, road features 16, buildings, house numbers and speed limits 8.
Places.
- The Overture read is pruned on Overture's
bboxcolumn, not on the geometry (2026-09-18). The region filter used to beST_X/ST_Y, which DuckDB decodes per row, so every place on earth was read for every region: 504 s of a 570 s Kentucky bake, once per region, 414 times a wave. The same filter against the plainbboxstruct lets row-group statistics skip everything outside the region, and the identical 400,608 rows came back in 3.7 s. The geometry test stays as the exact filter. With the scan down to seconds,max-parallelwent from 4 to 8. - The toolchain is cached and pinned. tippecanoe 2.79.0, go-pmtiles 1.31.2 and DuckDB sit in
~/vela-binunder the cache keybake-tools-<os>-tippecanoe-2.79.0-pmtiles-1.31.2-duckdb-latest. Every job used to build tippecanoe from source: 69 s of a roughly two-minute job, 414 times a wave. osmium and jq still come from apt. - Scratch goes on the big disk. A runner's root has about 14 GB free and
/mntabout 65 GB; a continent-sized region (Australia: a 619 MB AllThePlaces extract, a 1.3 GB OSM extract, DuckDB's spill and tippecanoe's temp files) fills root, and a runner whose disk fills reports only "the runner has received a shutdown signal".TMPDIR=/mnt/vela-workmoves all of it. DuckDB runs withmemory_limit = 11GBand a spill directory under the 16 GB runner, so a big box is a slower bake rather than a dead one. - The AllThePlaces decode streams. The region's z15 tiles come out of the world archive by range
request (
pmtiles extract), andtippecanoe-decodeis filtered withgrepbeforejqso it is read one feature at a time; parsing a dense country as one JSON document took the runner down. A cheap business-tag test runs first because AllThePlaces also carries national address registers (Belgium's decode was 7.1 million rows, nearly all addresses). - What the bake carries for a place is chapter 1's subject. The parts that decide rebake
behavior: a dropped duplicate still donates its hours, phone and website to the row that won
(
atpfillfor a chain locator,osmfillfor an OSM node, nearest same-name match only, never by brand), and every row carriesloc, its city, state and ZIP formatted the way the country writes them. Both arrived on 2026-09-22, so a region baked before that has neither until its next rebake. - An empty region publishes nothing. Uninhabited rows (Ashmore and Cartier) have no businesses, tippecanoe refuses an empty input, and the upload step skips them.
- The upload retries. GitHub's asset upload answers 500 on big archives often enough to fail a
run by itself (a 300 MB archive, twice on 2026-09-17), so it tries four times, waiting
30 s x trybetween attempts.
Basemap. planetiler is pinned to v0.10.2, downloaded with five retries and checked with
unzip -t, because the moving latest asset came back as something other than a jar on busy
runners and killed five jobs of the world bake. PLANETILER_XMX = 10g. A GitHub release asset
must stay under 2 GiB; Nunavut's z14 bake did not, so an archive at or over 2,147,483,648 bytes is
rebaked one zoom shallower, and fails loudly if it still does not fit. On the phone, an archive
shallower than FULL_MAP_ZOOM = 14 (read from byte 101 of its header) is used only offline, since
streaming draws that ground better.
World floor. world-lowzoom.yml bakes z0-7 (default maxzoom 7) against Monaco, the smallest
extract Geofabrik publishes: at those zooms the water, boundary and place layers come from
planetiler's Natural Earth base data, not from OSM, so a tiny input measures the global cost. The
result is about 11 MB (10.96 MB published). The app pulls it once alongside the first offline
download and keeps it out of the per-region candidate list (WORLD_ID = "world"), so losing
signal away from a downloaded region is a coarse map rather than a blank screen.
Routing. Routing-only obf, indexed lean (VelaObfShim), from an extract pre-filtered by
osmium tags-filter to highway ways, ferry and shuttle-train routes and restriction and route
relations: that cuts a US state to about a third of its bytes and a quarter of its nodes, which is
what fits the big rows under a 16 GB runner. JAVA_HEAP = 12g (above that the runner kills the
JVM), -XX:+UseParallelGC so a doomed region fails fast. Measured before the filter: Bavaria needed
-Xmx22g and 3 h 7 min on a 32 GB machine for a 694 MB obf. A dispatch writes to
obf-manifest-staging.json by default (staging: true), which the app never reads; copying staging
over obf-manifest.json flips the fleet's whole catalog at once. The conductor does that copy
itself, and only after a clean cycle: all three routing jobs finished with every region baked
(retries included), no live region missing from staging, no revision going backwards, every file
present on the release, and something actually newer. A cycle that gave up with regions missing
never flips. The manifest it replaces is kept as obf-manifest-previous.json; copying that back
over the live name is the rollback.
Every bake downloads its extract through scripts/fetch-pbf.sh. On 2026-09-30 Geofabrik
answered every -latest.osm.pbf with a redirect to the same name plus a slash, a plain download
followed it in a circle, and 213 of 231 place-pack jobs failed. The script tries the plain download,
then walks the redirects one hop at a time dropping the stray slash, and if they run in a circle it
reads the folder listing and takes the newest dated file for the region.
House numbers come from OpenAddresses, not OSM: the bake resolves each source's current job id
through the OpenAddresses batch API (ids rotate per refresh), and a source ending in /* folds every
county and city source under that prefix into one archive for states with no statewide source.
How the manifest is published¶
Places and basemap: derived from the release. The last job of a bake used to fold the run's own entries into whatever the manifest already said, and it sat in a concurrency group so parallel runs could not clobber each other. GitHub cancels a job that is pending in a concurrency group as soon as a newer one joins, so in a wave of runs the middle merges were killed after their archives had already been uploaded: on 2026-09-18, 10 of 25 basemap runs lost their merge and the manifest listed 99 of 414 regions; on 2026-09-22, 34 of 54 runs of a state-wide places wave lost theirs, published new archives under the previous bake's revisions, and mailed a failure each.
So the merge is now a repair. scripts/merge-places-manifest.sh and
scripts/merge-basemap-manifest.sh do nothing but exec repair-places-manifest.sh and
repair-basemap-manifest.sh with today's date and the run's entry directory. Each one:
- Lists every
places-*.pmtilesorbasemap-*.pmtilesasset on the release, with its size. - Keeps the old manifest's row for an archive that has not changed. For both, "unchanged" means
the same size in MB and an upload date no later than the row's
rev, because two bakes of a small state can round to the same size (Nebraska, 2026-09-22). The basemap repair used the size alone until the same day. - Builds a fresh row for anything else. Places take name and bounds from
places-regions.jsonand look for aplaces-<id>.<oldrev>.vpatchasset to carry as the delta. Basemap takes the name from the routing catalog and the bounds from the archive itself: one range request for the first 127 bytes, read byscripts/pmtiles-bbox.py(int32 E7 at bytes 102 to 117, min lon, min lat, max lon, max lat), then passed throughscripts/clamp-bbox.py. - Lets the run's own entry files win for the regions it baked, since they carry the true bake
revand any delta. - Uploads with
upload_manifest: up to five tries, sleepingRANDOM % 15 + 5seconds between them, because several merges finishing together race on the one asset (--clobberdeletes and re-uploads, so the loser sees a 422 "already exists" or a 404; two of nine parallel runs on 2026-09-22). - Lists the release again, and if an archive landed meanwhile, rebuilds (basemap once more; places up to three attempts).
The concurrency groups are gone from both merge jobs, and both merges run if: always(). The
manifest is now a function of what is published: running it twice changes nothing, a merge that
never ran costs nothing, and the next one puts everything back. A run with no entries of its own
still runs the repair and heals whatever an earlier run left out.
Everything else still folds. The obf, place pack and road features workflows serialize whole runs with a run-level concurrency group, and the building, house-number and speed-limit merges sit in their own merge-job groups; their merge scripts replace rows by id and keep the rest. They are dispatched far less often, in fewer and larger runs, which is what keeps the cancellation from biting them.
The Alaska box¶
Alaska's extract crosses the antimeridian (the Aleutians reach past 180), so every source that
derives a box from it (the PMTiles header, osmium's header box, Geofabrik's index) reports
longitude -180 to 180. Read literally, [49.8, -180, 73, 180] covers every point between 49.8 N
and 73 N on Earth. The address-overlay rule hides the basemap's own house-number layer wherever a
house-number overlay covers the view, so the Netherlands, Britain, Canada and Germany north of
Munich lost their house numbers to an empty Alaska overlay, while France and Spain, below the band,
kept theirs (issue #257, fixed 2026-09-22). There was never a data gap to fill.
Three layers of fix:
- The live manifests (house numbers, buildings, speed limits, basemap) were patched by hand to
E = -129.9, which fixed every installed build with no update. The places catalog already had a hand-set Alaska box. - The catalogs and bake scripts clamp it:
overlay-regions.jsonandaddress-regions.jsoncarry-129.9, andscripts/clamp-bbox.pyrewrites analaskabox whose west edge is at or below -179 and east edge at or above 179, in the basemap bake, the basemap repair and the speed-limit bake. - The app refuses a globe-wide box.
RegionPolys.boxCoversonly covers whene - w < WORLD_SPAN(350.0) orn - s >= WORLD_BAND(120.0). The second clause keeps a real whole-world row, like the world floor, working.
Which region a point is in¶
Since 2026-09-21 (issue #599) a region is picked by the polygon its extract was cut with, not its
box. scripts/region-polys.py fetches the .poly Geofabrik publishes beside every extract in the
routing catalog, simplifies each to TOL_DEG = 0.05 (about 5 km), and writes
app/src/main/assets/region_polys.json (458 regions, about 340 KB). RegionPolys.covers(id, lat,
lng) answers from it, or null for an id it has no polygon for (the building catalog, or a row added
since the last run of the script), and the region stores and catalogs then fall back to boxCovers (road features uses a plain
box test). The tie-break
among covering regions is still the smallest box. The trigger: Vietnam's extract carries the island
claims, so its box reaches 114.6 E and swallows Hong Kong, and "download the area you're viewing"
from Hong Kong announced Vietnam. RegionPolysTest fails if a catalog id is missing from the asset,
so the script has to be rerun whenever a row is added.
How your phone picks up a new build¶
Streamed data (places, buildings, house numbers and speed limits while online; the basemap
archive is never streamed, since online the map comes from OpenFreeMap) is read
by HTTP range requests against the same URL, so a rebuilt archive is picked up as soon as the cache
lets go of the old bytes. Forcing it is a matter of clearing the map cache from Settings > Offline
maps. The places and basemap stores cache each manifest for MANIFEST_TTL_MS = 60 min and remember
a failed fetch for MISS_MEMO_MS = 10 min, since the lookup runs on every camera idle. The cache
expires on purpose: a bake publishes while the app is running, and a process that lives for days
would otherwise never offer the Update (found on a device, a rebake four minutes old and no Update).
Downloaded data stays exactly as downloaded, which is the point of downloading it. Each
installed file's revision is recorded beside it (revs.json per store), and
MapViewModel.refreshRegionUpdates compares each downloaded region's pieces against the
manifests when Offline maps opens and again after any download or update: the obf by the routing row's rev (only when an installed rev
is known), and every places and basemap archive whose box center falls inside the region's polygon.
A region with anything newer, or with a places or map archive that never finished downloading,
gets an Update button (a newer place pack alone does not show it; Update refreshes the pack
when it is there). One tap refreshes, in order, the place pack,
the places archives, the basemap archives and the routing file.
- Revisions. Places, basemap and obf rows carry
revas the bake date,YYYYMMDD. Place packs count instead:revis the live row's plus one. Road features carry anupdatedAtstamp, and the app re-downloads a region's file on its own whenever the stamp differs (manifest cachedMANIFEST_TTL_MS = 6 h,MAX_LOADED = 4regions in memory). The camera dataset carries aversion;FlockCameras.refreshruns at app start and downloads it only when it beats both the downloaded copy and the floor bundled in the APK. - Same-day caveat. Two bakes of a region on the same UTC day share a
rev. The second one overwrites the archive but the manifest's rev does not move, so a phone that downloaded the first bake that morning is never offered the second, and no patch is published between them. The places workflow'srevinput overrides the stamp for testing.
Delta updates (bake publishing since 2026-09-18). A week of OpenStreetMap edits moves about one tile in a hundred, so a places rebake publishes a patch against the archive it replaces instead of making every downloader take a few hundred MB again. On 2026-09-22 the live places manifest listed 416 regions, 36 of them with a patch.
- Built and proven in the bake, or not published. The job fetches the published archive and
its
revbefore baking, runsscripts/pmtiles-make-patch.py, applies the result to a copy withpmtiles-apply-patch.py --verify, and publishes only if the result carries the new archive's fingerprint and the patch is under a third of the archive. It is uploaded asplaces-<id>.<fromRev>.vpatchand the row gainsdelta: {fromRev, url, sizeMb}. A rebake that also carries a change to the bake script usually fails the one-third test and gets no patch (Guernsey and Jersey the day after the OSM source landed: 2506 of 4586 tiles, 2.17 MB against 3.3 MB, refused); nothing checks for script changes as such. - The map bake does the same since 2026-09-28 (
basemap-<id>.<fromRev>.vpatchonbasemap-tiles), fetching the old archive after planetiler finishes so the two do not share the runner's disk.repair-basemap-manifest.shfinds the patch by name like the places repair.places-churn.ymlmeasures real churn by baking one region twice against OSM extracts N days apart (Andorra, six days: 11% of tiles, 25% of bytes, a delta at 22% of a full download). - Applied in place.
PmtilesPatch(formatVELAPTCH,VERSION = 2) appends the changed tile blobs and the rebuilt directory past the end of the file,fsyncs, checks the fingerprint (SHA-256 over sorted tile ids, run lengths and tile hashes) against the directory it just wrote, and only then rewrites the 127-byte header. An interrupted apply leaves the old archive intact and longer. The patch names no offsets, only whether each tile rides in the patch or is already in the archive under that id, so it lands on an archive an earlier patch or a compaction already moved. - Only from the exact revision. The installed
revmust equal the patch'sfromRev; a gap, a refusal or a fingerprint mismatch falls back to downloading the region whole. - Dead space is reclaimed locally. A patch leaves the replaced tiles behind, counted per archive
in
dead.json. Past a fifth of the file (DEAD_LIMIT_DIVISOR = 5),PmtilesCompactrewrites it in tile order into a temporary file, needing the archive's size plusMARGIN_BYTES = 32 MBfree, checks the fingerprint and swaps it in. Past half the file in dead bytes, the delta is refused and the region comes down whole.adb shell setprop debug.vela.compact truecompacts after every patch. - Policy is the user's, and on Wi-Fi by default. Settings > Offline maps > "Update downloaded
regions": "Never on its own" (
RegionUpdates.Mode.OFF), "On Wi-Fi" (an unmetered network, as the system judges it; the default since 2026-09-25) or "On Wi-Fi and mobile data". It was off until somebody had watched a patch download and apply on a real phone, which happened on 2026-09-19. On Wi-Fi or mobile the app checks a minute after it starts (AUTO_PATCH_DELAY_MS = 60_000), every 3 hours after that (AUTO_PATCH_POLL_MS), and 15 s after a validated network appears; a check that read the manifests waits 20 hours (AUTO_PATCH_EVERY_MS) before the next one, a check that could not read them retries. Then every installed places or basemap archive and place pack whose manifest publishes a patch from the installed revision takes it quietly; routing files publish no patches and a full re-download is never automatic. A tap on Update always tries the patch first, whatever the mode; without one it downloads the archive whole, over the installed copy, which stays until the new one is complete. Every attempt is recorded in the diagnostics ring (kinddelta,auto:for the daily pass) and in logcat underVelaDelta. - Place packs have their own deltas.
poipack_delta.pypublishes a row-level SQLite delta (<id>.delta.zip) only when it is under half the full pack, and the app applies it whenever the installed pack's rev equalsfromRev, independent of the setting above. Basemap, obf and the overlays have no delta: an update is a full download.
So a fix that lands in OpenStreetMap reaches people in this order: the region's day comes round (within a week), streaming users see it once their cache lets go, and people who downloaded the region see an Update the next time they open Offline maps, within the hour of the bake.
What the update card is telling you¶
An update of a downloaded region runs up to four files in a row: the place pack (offline search), the places file (the map's own places), the map, and routing. Each tries a small update first. The card shows three different things, and says which:
- a percent while something downloads, small update or whole file;
- "Writing the ... update" with a moving bar and no percent while the small update is checked and written into the file already on the phone. For the places file and the map this includes re-reading the whole file to prove the result matches a fresh download, which on a slow head unit takes minutes;
- "The small update did not fit. Downloading ..." with a percent starting from 0 when that check fails and the whole file is fetched instead.
Until 2026-09-30 all three shared one title, so the bar reached 100, sat still, and then counted up again with no explanation.
Retired data¶
routing-graphs (GraphHopper CH graphs, about 270 assets) is the previous generation of offline
routing, replaced by obf-regions on 2026-09-15. Nothing in the app reads it and the workflow that
built it is gone. The first launch after that update deletes the old graphs from the phone and
tells the user to download their regions again (LegacyGraphs.purge). The release itself is still
hosted, unreferenced.
Limits¶
- A bad row in a source dataset lives until the next bake. For places that is up to a week for the OSM and AllThePlaces sides; for buildings, house numbers and speed limits, up to a quarter; for routing, until someone dispatches it. Fixing it in OpenStreetMap is the durable route, and it is why OSM wins the coordinate in the places bake.
- Overture publishes monthly, so "rebake sooner" does not mean "fresher" for the fields that come from Overture. It does for the AllThePlaces and OSM halves.
- The routing bake is manual and memory-bound. A region whose filtered extract still does not fit 12 GB of heap has no routing file, and the obf catalog changes only when someone dispatches it and copies staging over live. If a region's roads have changed materially, dispatching that one region is the fix.
- Nothing is versioned per user. Everyone on a given day gets whatever the release currently holds, which is why rebakes overwrite in place rather than accumulating generations. A patch exists only from the immediately previous revision, so a phone two rebakes behind takes the whole file.
- The data releases are huge to list. Each of
obf-regions,places-overlays,basemap-tilesandroad-featuresholds about 450 assets, about 780 KB of release JSON apiece, and they sort to the top of the release list because they are republished constantly. The app updater therefore reads app tags from the refs endpoint and never lists releases (a canary check measured three requests, 208 KB); anything else that lists releases has to paginate or bound by tag.