Map data pipeline
Everything the server serves — vector tiles, search index, routing graph, terrain, fonts, sprites — is generated into data/ by the Node scripts in data/pipeline/. Nothing under data/raw/, data/build/, data/fonts/, data/routing/ or data/cache/ is committed (.gitignore); the deployment mounts the built directory read-only at /data (.env.prod.example, UZMAPS_DATA).
The primary source is the OpenStreetMap extract for Uzbekistan from Geofabrik. GeoNames, Overture Places, Microsoft building footprints and the Copernicus DEM are supplements. Each has its own licence; see Sources and licences.
What you need#
| Requirement | Why |
|---|---|
| Node 22+ (24 recommended) | All steps are .mjs scripts. sprites.mjs and emit-style.mjs import TypeScript from packages/cartography/src directly and rely on Node's type stripping. |
| Go 1.24+ | index, buildings and furniture run go build of services/server to bin/uzmap-new(.exe) before calling it. |
| Docker | Planetiler (tiles), Valhalla (routing), GDAL (terrain), DuckDB in python:3.12-slim (overture), tippecanoe (places, buildings, furniture). |
| Memory | Planetiler runs with -Xmx6g by default; the DuckDB container is capped at 3 GB; tippecanoe builds are sized against an ~8 GB Docker Desktop VM. |
| Disk | In this checkout: data/raw 3.9 GB, data/build 2.3 GB, data/routing 239 MB, data/cache 119 MB, data/fonts 12 MB. |
The pipeline binary is bin/uzmap-new(.exe), not bin/uzmap(.exe): the latter is the binary a running server was started from and Windows refuses to overwrite it. Override with UZMAP_BIN.
Running it#
npm install
npm run data:all # every step, in order
node data/pipeline/all.mjs --list # steps, required/optional, one-line description
node data/pipeline/all.mjs --dry-run # print the plan, run nothing
node data/pipeline/all.mjs --only=overture,index,places
node data/pipeline/all.mjs --from=index --strict # --strict: exit 2 if an optional step fails
node data/pipeline/furniture.mjs # not part of data:all, see below
npm run style:emit # static style JSON, not part of data:allall.mjs runs each step as a child node data/pipeline/<step>.mjs. A failed required step stops the run and the remaining steps are reported as skipped; a failed optional step is reported and the run continues. Exit code 1 means a required step failed; 2 means only optional steps failed and --strict was given (or an unknown argument was passed). Flags: --dry-run/-n, --only=a,b, --skip=a,b, --from=step, --strict, --list.
Order is not alphabetical and it matters: overture must precede index, which consumes data/raw/overture/places.geojsonl, and places must follow index, which writes its input.
| # | Step | npm script | Required | Tool | Output |
|---|---|---|---|---|---|
| 1 | download | data:download | yes | fetch | data/raw/uzbekistan-latest.osm.pbf, geonames-UZ.zip, geonames-alternatenames-UZ.zip |
| 2 | fonts | data:fonts | yes | fetch | data/fonts/<stack>/<range>.pbf |
| 3 | sprites | data:sprites | yes | sharp | data/sprites/sprite[@2x].{png,json} |
| 4 | overture | data:overture | no | DuckDB (Docker) | data/raw/overture/places.geojsonl |
| 5 | tiles | data:tiles | yes | Planetiler (Docker) | data/build/uzbekistan.pmtiles |
| 6 | index | data:index | yes | uzmap index (Go) | data/build/search.sqlite, places-overture.geojsonl, places-custom.geojsonl |
| 7 | places | data:places | no | tippecanoe (Docker) | data/build/places-overture.pmtiles |
| 8 | routing | data:routing | yes | Valhalla (Docker) | Docker volume uzmap-valhalla; copies in data/routing/ |
| 9 | buildings | data:buildings | no | uzmap buildings + tippecanoe | data/build/buildings-ml.pmtiles |
| 10 | terrain | data:terrain | no | GDAL (Docker) | data/build/terrain/{z}/{x}/{y}.png |
| — | furniture | (none) | not in all.mjs | uzmap furniture + tippecanoe | data/build/furniture.pmtiles |
| — | emit-style | style:emit | not in all.mjs | cartography | data/build/styles/uzmap-{light,dark}-{uz,ru,en}.json |
Rerunning#
- Downloads skip a destination that already exists and is non-empty. To refresh the OSM extract or GeoNames, delete the file first. Interrupted downloads leave a
.partfile that is discarded on the next run; each download retries up to 5 times with 1 s → 30 s backoff, verifiesContent-Lengthwhen the server sends one, and aborts after 60 s without data. overturerefuses to overwrite an existingplaces.geojsonlunlessUZMAP_FORCE=1is set.- Stop a running
uzmap servebefore rebuilding any.pmtiles. The server memory-maps the archives, so Windows will not let Planetiler or tippecanoe replace the file, and the tool's own error names neither the lock nor the server.tiles.mjs,buildings.mjsandfurniture.mjscheck for this and fail with a message that says so (EPERM/EBUSYon the pre-delete).
Steps#
Timings below are from the logs kept in data/*.log and from output-file timestamps in this checkout, on a 16-thread machine running Docker Desktop on Windows. They are indicative, not guarantees.
1. download — OSM extract and GeoNames#
data/pipeline/download.mjs. Fetches three files into data/raw/:
| URL | Destination | Size here |
|---|---|---|
https://download.geofabrik.de/asia/uzbekistan-latest.osm.pbf (override: UZMAP_OSM_URL) | uzbekistan-latest.osm.pbf | 124 MB |
https://download.geonames.org/export/dump/UZ.zip | geonames-UZ.zip | 367 KB |
https://download.geonames.org/export/dump/alternatenames/UZ.zip | geonames-alternatenames-UZ.zip | 223 KB |
Time: bandwidth-bound; the GeoNames files took about 3 s (data/download.log).
2. fonts — Noto Sans glyphs#
data/pipeline/fonts.mjs. Downloads pre-built SDF glyph ranges for Noto Sans Regular, Noto Sans Medium and Noto Sans Italic from https://raw.githubusercontent.com/protomaps/basemaps-assets/main/fonts/<stack>/<start>-<end>.pbf, 256 ranges per stack (0-255 … 65280-65535), 768 files in total, 24 in parallel. A range the upstream does not have (HTTP 404) is written as an empty file so it is not requested again. Existing files are skipped. Writes data/fonts/LICENSE.txt.
Output: data/fonts/<stack>/<range>.pbf (12 MB). Served at /fonts/. Time: 11 s (data/fonts-download.log).
3. sprites — icon sheets#
data/pipeline/sprites.mjs. Imports badgeIcons, rawIcons and customGlyphs from packages/cartography/src/icons.ts, then renders with sharp:
- badge icons — a circle in the category-group colour with a white glyph, named
poi-<name>. The glyph comes frompackages/cartography/icons/glyphs/<name>.svgwhen it is a custom glyph, otherwisenode_modules/lucide-static/icons/<name>.svg. - raw icons — SVGs from
packages/cartography/icons/raw/; those flaggedsdfare converted to signed-distance fields soicon-colorworks at runtime. Stretchable icons carrycontent,stretchXandstretchYin the JSON.
Output: data/sprites/sprite.png, sprite.json, sprite@2x.png, sprite@2x.json, LICENSE.txt. The current sheet has 104 icons, 8 of them SDF. Served at /sprites/. Time: not logged; a local render.
4. overture — Overture Places for the Uzbekistan bbox#
data/pipeline/overture.mjs. Queries the Overture Maps GeoParquet release directly on S3 with DuckDB, run inside python:3.12-slim (pip install duckdb, then a generated data/raw/overture/fetch-places.py). The container is started with --memory=3g --cpus=2; DuckDB is set to memory_limit='2GB', threads=2. The earlier attempt with the overturemaps CLI ran out of memory in the Docker VM, which is why DuckDB is used instead.
Source: s3://overturemaps-us-west-2/release/<release>/theme=places/type=place/* (anonymous, region us-west-2). The bbox struct column is part of the Parquet row-group statistics, so a WHERE bbox.xmin … bbox.ymin … predicate skips every row group outside the bbox and the scan moves a few hundred MB rather than the global theme. The bbox is processed in longitude slices for visible progress and low peak memory.
| Variable | Default | Meaning |
|---|---|---|
UZMAP_OVERTURE_RELEASE | 2026-08-19.0 | release id |
UZMAP_OVERTURE_BBOX | 55.9,37.1,73.2,45.7 | xmin,ymin,xmax,ymax |
UZMAP_OVERTURE_SLICES | 4 | longitude slices |
UZMAP_DUCKDB_VERSION | (latest) | pip version spec, e.g. ==1.4.1 |
UZMAP_FORCE | unset | 1 rebuilds an existing output |
Output: data/raw/overture/places.geojsonl — one GeoJSON Feature per line, Point geometry, every Overture column kept as a property (id, categories, confidence, websites, emails, socials, phones, brand, addresses, names, sources, operating_status, basic_category, taxonomy, version) — plus places.meta.json (release, bbox, feature count, property list, DuckDB version, fetch time, licence, attribution). pip wheels and DuckDB extensions are cached in data/cache/pip and data/cache/duckdb.
In this checkout: 27,661 places, 37 MB. Time: 94 s inside the container, about 2 min wall (data/overture-download.log).
5. tiles — vector basemap#
data/pipeline/tiles.mjs. Runs Planetiler's OpenMapTiles profile in Docker:
docker run --rm -e JAVA_TOOL_OPTIONS=-Xmx6g -v <data>:/data ghcr.io/onthegomap/planetiler:latest
--osm-path=/data/raw/uzbekistan-latest.osm.pbf
--output=/data/build/uzbekistan.pmtiles
--download --download-dir=/data/raw/planetiler-sources
--water-polygons-url=https://osmdata.openstreetmap.de/download/simplified-water-polygons-split-3857.zip
--languages=uz,uz-Cyrl,uz-Latn,ru,en,kaa,kk,tg,de,fr --transliterate=true
--maxzoom=16 --mmap-temp=false --building-merge-z13=false --forceHeap: UZMAP_PLANETILER_XMX (default 6g). Requires the OSM extract; fails early if it is missing.
Two settings are deliberate:
--maxzoom=16rather than Planetiler's default 14. Street zooms overzoom a z14 tile three to four times and magnify the simplification baked in at z14 — which is what put building footprints across roads.--mmap-temp=false. At z16 the memory-mapped temp reader overflows its 32-bit offsets once a sort chunk passes 2 GB (NegativeArraySizeException); plain IO has no such limit.
--download fetches the profile's other inputs once into data/raw/planetiler-sources/: natural_earth_vector.sqlite.zip (434 MB, naciscdn.org), lake_centerline.shp.zip (81 MB, github.com/acalcutt/osm-lakelines), water-polygons-split-3857.zip (24 MB, osmdata.openstreetmap.de). Planetiler's scratch directory is data/tmp/, and it leaves data/tile_weights.tsv.gz (5.7 MB) behind.
Output: data/build/uzbekistan.pmtiles, PMTiles v3, z0–16, 543 MB, OpenMapTiles layers aerodrome_label, aeroway, boundary, building, housenumber, landcover, landuse, mountain_peak, park, place, poi, transportation, transportation_name, water, water_name, waterway (read from the archive header and metadata). Served at /tiles/{z}/{x}/{y}.pbf, /tiles/tilejson.json and, for the pmtiles:// protocol, /tiles/uzbekistan.pmtiles with range requests.
Time: the earlier z14 configuration logged 23 min 37 s including first-time source downloads (data/tiles-build.log). The z16 archive in this checkout was written 20 minutes after its OSM extract was downloaded, with sources already cached (file timestamps; no log kept). A comment in furniture.mjs calls the build "hour-long", so allow up to an hour on slower hardware.
Note that docs/architecture.md still describes the tileset as z0–14; the script and the built archive say z0–16.
6. index — search and geocoding#
data/pipeline/index.mjs. Builds the Go server, then runs:
uzmap index --pbf data/raw/uzbekistan-latest.osm.pbf --out data/build/search.sqlite
[--geonames data/raw/geonames-UZ.zip] [--geonames-alt data/raw/geonames-alternatenames-UZ.zip]
[--overture data/raw/overture/places.geojsonl --overture-out data/build/places-overture.geojsonl]
[--datasets datasets --datasets-out data/build/places-custom.geojsonl]Each optional input is passed only when the file or directory exists. uzmap index also accepts --overture-min-confidence (default 0.5), which the script does not set.
Merge order, and why: OSM entries first; GeoNames enriches OSM places with multilingual names and populations; Overture places are then applied, deduplicating against existing OSM POIs; first-party datasets/ records are merged last — a custom record that duplicates an existing POI (OSM or Overture) replaces it, so a curated record is never shadowed by a bulk one. Overture places are kept only when confidence ≥ 0.5, the category maps onto the UzMap taxonomy, the address country code is empty or UZ, and they do not duplicate an OSM POI.
Logged run (data/index2.log): 1,529 admin/multipolygon relations; 17.2 M nodes cached; 327,431 way entries, 50,041 street ways; 764 admin polygons; 8,306 GeoNames places loaded, 5,800 OSM places enriched; Overture 27,661 read, 11,573 added (7,863 low-confidence, 1,846 unmapped category, 5,396 outside Uzbekistan, 983 duplicates of OSM); 23,223 merged streets; 363,740 entries in 68 s. Earlier runs took 64–81 s. Add the go build.
Outputs:
| File | Contents |
|---|---|
data/build/search.sqlite (164 MB) | the index; metadata keys built_at, source, attribution |
data/build/places-overture.geojsonl (2.4 MB) | the added Overture POIs as Point features with properties name, icon, group, category, rank (lower = more prominent) |
data/build/places-custom.geojsonl (617 KB) | the added first-party POIs in the same shape |
The server does not read search.sqlite on every start: on first start it builds the in-memory index and writes data/build/search.cache (60 MB here) in the background, and reuses it while the SQLite file's modification time and size are unchanged. Rebuilding the index invalidates the cache automatically.
First-party datasets#
Drop operator-supplied place data in datasets/ (gitignored; "the repo tracks the importer, not the input"). The indexer picks up datasets/*.json and datasets/*/*.json — only .json; the .csv and .geojson siblings in this checkout are ignored. The supplied files are JSON arrays of records. Accepted fields:
| Field | Type | Notes |
|---|---|---|
id, type, name, description | string | name required; records without one are counted as MissingName |
latitude/longitude or lat/lon | number | either spelling; 0,0 is treated as missing |
categories | string[] | Russian-first vocabularies are mapped by osmx/datasets_ru.go |
main_category | string | |
address, country, region, city, street, house, postalCode | string | |
status, website, email, icon, phone | string | |
phones, gallery | string[] | gallery is stored joined with ; under custom:gallery |
socials, schedule, attributes, menu | raw JSON | passed through to the client untouched |
Records outside lon 55.9–73.2, lat 37.1–45.6 are dropped as data errors (uzBounds). A record from datasets/dataset_4.json, trimmed:
[
{
"id": "place_00031",
"name": "Nexus",
"type": "business",
"longitude": 69.259806,
"latitude": 41.339461,
"address": "Tashkent, Takhtapul Darvoza Street, 312",
"city": "Tashkent",
"street": "Takhtapul Darvoza Street",
"house": "312",
"categories": ["Sale and lease of commercial real estate"],
"phones": ["+998 94 581 97 97"],
"schedule": [{ "day": "Monday", "hours": "08:00–17:00" }],
"gallery": ["images/50629e91a3e982886f13f0c436a676c5.jpg"]
}
]To see what an unfamiliar file contains before importing it, node data/pipeline/inspect.mjs <dir-or-file> [--rows=5] reports format, record count, field names with inferred types and fill rates, geometry type and sample rows. It reads at most 8 MB of any file and writes nothing.
7. places — POI overlay tiles#
data/pipeline/places.mjs. Requires data/build/places-overture.geojsonl (fails with a message pointing at data:overture then data:index if absent). Concatenates it with places-custom.geojsonl, when present, into data/build/places-all.geojsonl: one layer keeps the rank-based zoom gating coherent, whereas two overlapping POI layers would each thin themselves independently and look twice as crowded.
The script streams the input once to collect rank values and derives per-zoom thresholds from the rank distribution, then writes a $zoom-aware filter to data/build/places-overture.filter.json. At z14 everything passes; at z13 only the top UZMAP_POI_KEEP_Z13 fraction (default 0.25); at z12 the top UZMAP_POI_KEEP_Z12 (default 0.08); features without a rank pass at every zoom. Set UZMAP_POI_RANK_DESC=1 if a higher rank means more prominent.
tippecanoe is then run with -o /data/build/places-overture.pmtiles -l poi -Z12 -z14 -r1 --no-feature-limit --no-tile-size-limit -J <filter> --order-by=rank --buffer=16 --force. -r1 disables tippecanoe's pseudo-random point thinning so the filter alone decides what survives at low zoom; --order-by=rank puts prominent POIs first in each tile so they win label collisions.
Output: data/build/places-overture.pmtiles (1.4 MB, layer poi, z12–14) and places-overture.meta.json (layer, minzoom, maxzoom, features, rankThresholds, sources, licence, attribution). In this checkout: 14,401 features, thresholds rank <= 3 at both z13 and z12. Time: about 30 s (file timestamps).
8. routing — Valhalla graph#
data/pipeline/routing.mjs. Writes data/routing/build.sh, creates the Docker volume, and runs the script in the Valhalla image:
docker volume create uzmap-valhalla
docker run --rm -v uzmap-valhalla:/valhalla -v <data>:/data --entrypoint bash ghcr.io/valhalla/valhalla:latest /data/routing/build.shThe graph is built inside a named volume rather than the bind-mounted data/ because Valhalla memory-maps its tiles and SQLite databases, which is unreliable on Docker Desktop bind mounts. The finished valhalla.json and valhalla_tiles.tar are copied to data/routing/ as portable artefacts (239 MB); the running Valhalla container reads the volume, not these copies.
build.sh runs valhalla_build_config (tile dir and extract under /valhalla, timezone and admin SQLite files, concurrency from UZMAP_VALHALLA_THREADS, service limits: auto 2,000,000 m and 50 locations, pedestrian 250,000 m, bicycle 500,000 m, minimum reachability 50), valhalla_build_timezones (which downloads the timezone polygon shapefile) if not already present, valhalla_build_admins, valhalla_build_tiles, valhalla_build_extract.
| Variable | Default |
|---|---|
UZMAP_VALHALLA_IMAGE | ghcr.io/valhalla/valhalla:latest |
UZMAP_VALHALLA_VOLUME | uzmap-valhalla |
UZMAP_VALHALLA_THREADS | 4 |
Time: 1 min 33 s, 901 tiles (data/routing-build.log).
9. buildings — Microsoft ML footprints#
data/pipeline/buildings.mjs. Three stages:
- Download. Fetches
https://minedbuildings.z5.web.core.windows.net/global-buildings/dataset-links.csv, keeps rows whoseLocationisUzbekistan(125 parts), downloads each gzipped part and decompresses it todata/raw/ms-buildings/<quadkey>.geojsonl.UZMAP_MS_CONCURRENCYparallel downloads (default4). Complete parts are skipped on rerun. About 2.4 GB decompressed on disk here. - De-duplicate against OSM.
uzmap buildings --pbf data/raw/uzbekistan-latest.osm.pbf --ml-dir data/raw/ms-buildings --out data/build/buildings-ml.geojsonldrops footprints that overlap an OSM building, sit on an OSM road corridor, duplicate another ML footprint, or are smaller than--min-area(default 12 m²) (services/server/internal/mlbuildings). Two overlapping extrusions z-fight in the 3D view, which is worse than a missing building. Logged: 1,629,786 OSM footprints as the reference set; 6,504,180 ML footprints read, 5,721,350 kept (670,231 OSM overlaps, 26 ML duplicates, 112,573 tiny) in 291 s (data/ml-merge.log; that run predates the road-corridor test). Output 1.02 GB. - Tiles. tippecanoe with
-o /data/build/buildings-ml.pmtiles -l building -Z13 -z16 --no-feature-limit --maximum-tile-bytes=6000000 --detect-shared-borders --simplification=4 --buffer=8 --no-simplification-of-shared-nodes -pS --force. z16 rather than z14 for the same overzoom reason as the basemap; the 6 MB tile cap exists because without any cap the z13–14 overview tiles grow unbounded in memory and the run dies against the Docker VM.
Output: data/build/buildings-ml.pmtiles, layer building, z13–16. Time: download about 3 min, merge 4 min 51 s; the z16 tiling is not logged and the archive is absent from this checkout at the time of writing (the server log of 3 September shows an earlier z13–14 build). README.md budgets about 25 min for the whole step.
The tippecanoe image#
places, buildings and furniture share tippecanoe() in _util.mjs. The image is resolved in this order: UZMAP_TIPPECANOE_IMAGE; an already-present ghcr.io/felt/tippecanoe:latest, uzmap/tippecanoe:local or tippecanoe:latest; docker pull of the upstream image (which currently answers "denied" for anonymous pulls, data/tippecanoe-pull.log); otherwise a build from the upstream source tarball into data/raw/tippecanoe-src/ with a generated ubuntu:24.04 Dockerfile, tagged uzmap/tippecanoe:local, taking about 5 min. tippecanoe's temporaries go in the named volume uzmap-tiletmp because they are memory-mapped SQLite-backed files that fail on a Windows bind mount once a build spills to disk.
10. terrain — Copernicus DEM to Terrain-RGB#
data/pipeline/terrain.mjs. Downloads the Copernicus DEM 1°×1° COG cells covering lat 37–45, lon 55–73 (171 cells) from https://<bucket>.s3.amazonaws.com/<name>/<name>.tif, where <name> is Copernicus_DSM_COG_<code>_N<lat>_00_E<lon>_00_DEM. A 404 means a cell with no land data and is not an error.
| Variable | Default | Effect |
|---|---|---|
UZMAP_DEM | unset (GLO-90) | 30 selects GLO-30: bucket copernicus-dem-30m, name code 10, warp resolution 38 m, about 8× more data |
UZMAP_TERRAIN_MAXZ | 11 (GLO-90) or 12 (GLO-30) | top zoom generated |
UZMAP_DEM_CONCURRENCY | 4 | parallel downloads |
Cells land in data/raw/dem-90m/ (or dem-30m/). The script then writes data/build/terrain-build.sh and runs it in ghcr.io/osgeo/gdal:ubuntu-small-latest: gdalbuildvrt → gdalwarp to EPSG:3857 over extent 6120000 4430000 8130000 5780000 at 76 m (38 m for GLO-30), bilinear, no-data filled with 0 → three gdal_calc.py passes encoding Mapbox Terrain-RGB (v = (h + 10000) * 10; R = v >> 16, G = (v >> 8) & 255, B = v & 255) → gdal2tiles.py --xyz -z 4-<MAXZ>.
Output: data/build/terrain/{z}/{x}/{y}.png and terrain/meta.json (encoding: "mapbox", minzoom: 4, maxzoom, source, attribution). Served at /terrain/{z}/{x}/{y}.png; /terrain/tilejson.json is built from meta.json and returns 404 with a hint if it is missing. Time: 10 min 44 s for GLO-90 with 53 of 171 cells still to download, of which gdal2tiles took 5 min 28 s (data/terrain-build.log).
furniture — crossings, signals, trees, bus stops (not in data:all)#
data/pipeline/furniture.mjs. There is no data:furniture npm script and all.mjs does not list the step, yet .env.prod.example expects build/furniture.pmtiles and the engine turns the layer on when the server advertises it. Run it by hand:
node data/pipeline/furniture.mjsIt builds the server, runs uzmap furniture --pbf data/raw/uzbekistan-latest.osm.pbf --out data/build/furniture.geojsonl, then tippecanoe with -o /data/build/furniture.pmtiles -l furniture -Z 13 -z 16 --no-feature-limit --maximum-tile-bytes=2000000 --drop-densest-as-needed --force. OpenMapTiles carries none of these point features, and extracting them separately avoids rerunning the basemap build to add three point layers.
Output: data/build/furniture.pmtiles (1.3 MB, layer furniture, z13–16). Time: about 2 min (file timestamps).
emit-style — static style JSON (npm run style:emit)#
data/pipeline/emit-style.mjs calls buildStyle({ theme, language, serverUrl }) from packages/cartography/src/index.ts for light/dark × uz/ru/en and writes data/build/styles/uzmap-<theme>-<language>.json for clients that cannot run the TypeScript style builder (native MapLibre SDKs). UZMAP_SERVER_URL (default http://localhost:8080) is baked into the source URLs. Served at /styles/.
Environment variables#
| Variable | Step | Default |
|---|---|---|
UZMAP_OSM_URL | download | https://download.geofabrik.de/asia/uzbekistan-latest.osm.pbf |
UZMAP_PLANETILER_XMX | tiles | 6g |
UZMAP_OVERTURE_RELEASE | overture | 2026-08-19.0 |
UZMAP_OVERTURE_BBOX | overture | 55.9,37.1,73.2,45.7 |
UZMAP_OVERTURE_SLICES | overture | 4 |
UZMAP_DUCKDB_VERSION | overture | latest |
UZMAP_FORCE | overture | unset |
UZMAP_BIN | index, buildings, furniture | bin/uzmap-new(.exe) |
UZMAP_POI_KEEP_Z13 / UZMAP_POI_KEEP_Z12 | places | 0.25 / 0.08 |
UZMAP_POI_RANK_DESC | places | unset |
UZMAP_TIPPECANOE_IMAGE | places, buildings, furniture | resolved automatically |
UZMAP_MS_CONCURRENCY | buildings | 4 |
UZMAP_VALHALLA_IMAGE / UZMAP_VALHALLA_VOLUME / UZMAP_VALHALLA_THREADS | routing | ghcr.io/valhalla/valhalla:latest / uzmap-valhalla / 4 |
UZMAP_DEM / UZMAP_TERRAIN_MAXZ / UZMAP_DEM_CONCURRENCY | terrain | GLO-90 / 11 or 12 / 4 |
UZMAP_SERVER_URL | emit-style | http://localhost:8080 |
Output layout#
| Path | Written by | Read by the server as |
|---|---|---|
data/raw/uzbekistan-latest.osm.pbf | download | — (pipeline input) |
data/raw/geonames-UZ.zip, geonames-alternatenames-UZ.zip | download | — |
data/raw/overture/places.geojsonl, places.meta.json, fetch-places.py | overture | — |
data/raw/planetiler-sources/ | tiles (--download) | — |
data/raw/ms-buildings/dataset-links.csv, <quadkey>.geojsonl | buildings | — |
data/raw/dem-90m/ or dem-30m/ | terrain | — |
data/raw/tippecanoe-src/, tippecanoe-main.tar.gz | tippecanoe fallback build | — |
data/build/uzbekistan.pmtiles | tiles | /tiles/{z}/{x}/{y}.pbf, /tiles/tilejson.json, /tiles/uzbekistan.pmtiles |
data/build/<name>.pmtiles (any other) | places, buildings, furniture | /tiles/<name>/{z}/{x}/{y}.pbf, /tiles/<name>/tilejson.json, /tiles/<name>.pmtiles; listed in /api/status sources.tiles, with sources.places, sources.buildings_ml, sources.furniture booleans |
data/build/search.sqlite | index | loaded at start; missing → search disabled with a warning |
data/build/search.cache | the server, first start | reused while search.sqlite is unchanged |
data/build/places-overture.geojsonl, places-custom.geojsonl, places-all.geojsonl, places-overture.filter.json, places-overture.meta.json | index, places | — (intermediates) |
data/build/buildings-ml.geojsonl, furniture.geojsonl | buildings, furniture | — (intermediates) |
data/build/terrain/{z}/{x}/{y}.png, terrain/meta.json, terrain-build.sh | terrain | /terrain/, /terrain/tilejson.json; sources.terrain in /api/status |
data/build/styles/*.json | emit-style | /styles/ |
data/fonts/ | fonts | /fonts/ |
data/sprites/ | sprites | /sprites/ |
data/routing/build.sh, valhalla.json, valhalla_tiles.tar | routing | — (the live graph is in the uzmap-valhalla volume) |
data/cache/pip, cache/duckdb | overture | — |
data/cache/photos | the server (Wikimedia photo cache) | runtime |
data/tmp/, data/tile_weights.tsv.gz | Planetiler scratch | — |
data/shots/ | scripts/shot.mjs (visual QA), not the pipeline | — |
data/*.log | operator-redirected script output; the scripts themselves log to stdout | — |
The engine asks /api/status on load and, for each of sources.buildings_ml, sources.places and sources.furniture that is true, adds the matching /tiles/<name>/tilejson.json source; sources: { mlBuildings, places, furniture } on UzMap can force a URL or disable one. So an optional archive that is present is used, and one that is absent is silently not.
Sources and licences#
Licences below are as recorded in this repository, checked against the upstream licence page where noted. Three points need action and are called out after the table.
| Source | Used by | Licence |
|---|---|---|
| OpenStreetMap, Geofabrik extract | tiles, index, routing, buildings (reference set), furniture | ODbL 1.0 |
| OpenMapTiles schema, via Planetiler | tiles | Planetiler's output states: "Such tiles are reusable under CC-BY license granted by OpenMapTiles team" and "Maps made with these vector tiles must display a visible credit: © OpenMapTiles © OpenStreetMap contributors" |
| Natural Earth | tiles (low zoom) | public domain, no attribution required |
| OSM water polygons (osmdata.openstreetmap.de) and lake centrelines (acalcutt/osm-lakelines) | tiles | not recorded in this repository |
| GeoNames UZ + alternate names | index | CC BY 4.0 |
| Overture Maps Places, release 2026-08-19.0 | overture, index, places | per source dataset — see below; the repository labels it CDLA-Permissive-2.0 |
| Microsoft Global ML Building Footprints | buildings | CDLA Permissive 2.0 per the upstream README ("This data is licensed by Microsoft under the CDLA Permissive 2.0"); docs/data-sources.md agrees. The pipeline code says ODbL — see below |
| Copernicus DEM GLO-90 / GLO-30 | terrain | Copernicus DEM licence; free use with attribution. Credit recorded in the repository: "© DLR e.V. 2010–2014 and © Airbus Defence and Space GmbH 2014–2018 provided under COPERNICUS by the European Union and ESA" |
| Noto Sans glyph ranges (protomaps/basemaps-assets) | fonts | SIL Open Font License 1.1 |
| Lucide icons + UzMap original glyphs | sprites | ISC (Lucide) |
First-party datasets/ | index, places | the operator's own terms |
| Wikimedia Commons photos | server at runtime, not the pipeline; disable with uzmap serve --wikimedia=false | per image; the server assembles author · licence · "Wikimedia Commons" into an attribution field |
Tool licences, from README.md: Planetiler Apache-2.0, Valhalla MIT, MapLibre GL BSD.
Attribution as the system emits it#
| Where | Text |
|---|---|
| Generated style, basemap source | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> |
/tiles/tilejson.json | copied from the archive metadata: © OpenMapTiles © OpenStreetMap contributors (linked); falls back to an OSM link if an archive carries none |
Generated style, buildings-ml source | © <a href="https://github.com/microsoft/GlobalMLBuildingFootprints">Microsoft Building Footprints</a> |
buildings-ml.pmtiles metadata | © Microsoft Building Footprints (ODbL) |
Generated style, places source | © <a href="https://overturemaps.org">Overture Maps Foundation</a> |
places-overture.pmtiles metadata | © Overture Maps Foundation (CDLA-Permissive 2.0) + operator datasets |
Generated style, furniture source | no attribution property |
furniture.pmtiles metadata | © OpenStreetMap contributors |
/terrain/tilejson.json | © DLR/Airbus, Copernicus DEM (EU/ESA) from terrain/meta.json |
/api/status attribution | © OpenStreetMap contributors (ODbL); GeoNames (CC BY 4.0) from the index metadata; © OpenStreetMap contributors when no index is loaded |
Points that need attention#
- Microsoft footprints are labelled ODbL in the code but licensed CDLA Permissive 2.0 upstream.
data/pipeline/buildings.mjs(header and--attribution=© Microsoft Building Footprints (ODbL)) andservices/server/internal/mlbuildings/mlbuildings.gosay ODbL; the upstream README says CDLA Permissive 2.0 and does not contain the words "ODbL" or "Open Database License";docs/data-sources.mdalso says CDLA Permissive 2.0. The licence label embedded in the tile archive is therefore wrong. The visible credit in the style ("© Microsoft Building Footprints", linked to the repository) does not name a licence. - Overture Places is not one licence. Overture's attribution page gives no single Places licence: Meta, Microsoft and PinMeTo contributions are CDLA Permissive 2.0, Foursquare's are Apache 2.0 (with a
NOTICE.txtOverture links to), AllThePlaces is CC0 1.0. The raw extract in this checkout carriessources[].datasetvalues:meta26,768,AllThePlaces443,Foursquare342,Microsoft106,PinMeTo2 (every record also listsOverture).overture.godoes not readsources, andplaces-overture.geojsonlhas no such property, so once indexed there is no way to tell which of the 11,573 kept places came from which provider. Overture's page recommends the citation "Overture Maps Foundation, overturemaps.org". - The style credits OpenStreetMap but not OpenMapTiles. The Planetiler output and the archive metadata require "© OpenMapTiles © OpenStreetMap contributors";
style.tsdeclares only OpenStreetMap on the basemap source, while the TileJSON the same source points at carries both. Which one MapLibre displays when a source has both an inlineattributionand a TileJSONattributionhas not been checked here.
The Copernicus credit in terrain/meta.json is an abbreviation of the full wording quoted in terrain.mjs and docs/data-sources.md.