A fast OpenStreetMap renderer and real-time 3D map server written in Rust.
Load an .osm.pbf extract and it renders every tile on demand: rotated,
tilted and in 3D. It can also write print-quality posters or a static tile
pyramid offline.
| Financial District, z17 | Civic Center: roof shapes, mapped colours, parts |
|---|---|
![]() |
![]() |
| Brooklyn Bridge: decks on pillars over their shade | New York City, 184 MB extract |
|---|---|
![]() |
![]() |
WebGL client, like Google Maps
maps servekeeps the map in memory and builds geometry tiles on request: styled, triangulated ground, road ribbons, buildings and model instances (src/vtile.rs).- The browser draws every frame itself with WebGL2 and a perspective camera, so pan, zoom, rotate and tilt are smooth at 60 fps.
- Only tiles inside the view frustum load, near ones at full detail and far ones coarser, with haze toward the horizon and parent tiles standing in while children load.
- Tiles are compact: 16-bit positions in columns, normals derived in the shader, and flat-roofed buildings sent as outlines that the client extrudes. A Lower Manhattan z15 tile is 76 KB gzipped (it was over 500 KB with 32-bit float meshes).
- Buildings never vanish with distance: tiles below z15 carry simplified flat-roofed versions, fewer and larger as tiles get coarser, down to the skyline.
- Material 3 controls, light and dark themes.
- The server-rendered raster viewer is still available at
/raster.
True 3D, painted in depth order
- Buildings and
building:parts rise from their mappedmin_heightto theirheight, so skyscraper setbacks and floating parts appear where they are mapped. Untagged buildings get typical heights for their type. - Roofs follow
roof:shape: flat, gabled, hipped, pyramidal or cone, skillion, dome or onion. Each roof face is shaded under one sun, androof:colourandbuilding:colourare used when mapped. - Facades show floors as window bands. At night some windows are lit.
- Objects mapped on roofs (
location=roof, such as New York's wooden water tanks) stand on their building. - Bridges and viaducts are raised decks on pillars. Walls, hedges, fences and dams are vertical faces. Power lines and aerial tramways hang between pylons. Tree rows become rows of trees. Water lies below the land, so shorelines show banks.
- Everything with height is painted back to front, so a tall building in front hides the street, overpass or tower behind it. Tunnels are hidden in tilted views.
Bespoke models for street objects
- About 40 kinds of point objects have their own 3D model at real
dimensions:
- trees (with conifer variants)
- lamps, traffic signals with three lamps, stop signs
- power poles with crossarms, braced lattice pylons
- flagpoles with flags, chimneys, towers
- wooden water towers, cranes
- hydrants, benches, picnic tables, post boxes, bike racks
- bus shelters, phone booths, subway entrances with globe lamps
- swing sets, monuments, buffer stops, and more
Full cartography
- About 140 feature kinds, chosen from a census of every tag in the NYC
extract (
examples/census.rs). Only physical things are drawn; names, addresses and routes are not. - Golf courses, sport pitches by sport, swimming pools, parking stalls, road-surface areas, piers, platforms and aprons are all covered.
- Coastlines become land polygons, multipolygons keep their holes, and road casings merge cleanly at junctions.
- Two themes: Daylight and Midnight.
cargo build --release
# Real-time server and viewer: open http://localhost:8080
./target/release/maps serve nyc.osm.pbf --addr 0.0.0.0:8080 --cache-mb 1024
# A poster: 45° tilt by default, any bearing, any region
./target/release/maps render nyc.osm.pbf -o fidi.png \
--bbox=-74.017,40.702,-74.006,40.710 --zoom 17 --pitch 50 --bearing 29 --scale 2
# Straight down, the whole extract, dark theme
./target/release/maps render nyc.osm.pbf -o nyc.png --pitch 0 --theme dark
# A static tile pyramid (oblique 3D, Web Mercator aligned) with a Leaflet viewer
./target/release/maps tiles nyc.osm.pbf -o tiles --min-zoom 10 --max-zoom 17 --scale 2
# Feature counts and bounds
./target/release/maps info nyc.osm.pbf
# Build once into a snapshot; every command loads it in seconds
./target/release/maps build us-northeast.osm.pbf -o northeast.snap
./target/release/maps serve northeast.snapIn the viewer, drag to pan and scroll to zoom. Right-drag or Ctrl+drag
rotates and tilts; Shift+drag tilts. On touch screens, pinch to zoom, twist to rotate and
swipe two fingers vertically to tilt. The URL hash
(#zoom/lat/lon/bearing/pitch) is shareable.
Extracts for any region are available from BBBike or Geofabrik.
maps serve also exposes a JSON API under /v1, with interactive docs at
/v1/docs and the OpenAPI 3.1 spec at /v1/openapi.json.
| Path | Purpose |
|---|---|
/v1/info |
Bounds, center, data version, counts |
/v1/kinds |
Feature kinds in the extract with counts |
/v1/features?bbox=w,s,e,n&kind=building&page=1 |
GeoJSON features in a box (paged) |
/v1/features/{id} |
One feature |
/v1/lookup?lat=&lon=&radius= |
What is at a point, tallest first |
/v1/tallest?bbox= |
Tallest structures in a box |
/v1/static.png?lat=&lon=&zoom=&bearing=&pitch=&v= |
A 3D render as PNG; with v (the data version) it is cached forever (/v1/static also works) |
Features carry real attributes: height_m, min_height_m, roof_shape,
levels, colours, heading_deg for street furniture, and a third
coordinate (meters) on raised roads and rails. Errors are
{"detail": {"code", "message"}}.
curl 'localhost:8080/v1/tallest?bbox=-74.016,40.704,-74.008,40.712&limit=3'
curl -o fidi.png 'localhost:8080/v1/static.png?lat=40.7075&lon=-74.0115&zoom=17&bearing=29&pitch=50'The binary is self-contained, pure Rust, and has no system dependencies beyond libc. A multi-stage Dockerfile builds a 53 MB distroless image that runs as non-root:
docker build -t maps .
docker run -p 8080:8080 -v $PWD/nyc.osm.pbf:/data/map.osm.pbf:ro maps
# or: MAP=./nyc.osm.pbf docker compose up --buildEndpoints:
| Path | Purpose |
|---|---|
/ |
WebGL viewer |
/raster |
Raster viewer (server-rendered tiles) |
/meta.json |
Bounds, styles, data version |
/geo/{version}/{theme}/{z}/{x}/{y}.bin |
Geometry tiles (gzip) |
/models/{version}/{theme}.bin |
Model templates for instances |
/tiles/{version}/{style}/{bearing}/{pitch}/{z}/{x}/{y}[@2x].png |
Tiles. Immutable and cacheable forever, because version changes with the data. |
/healthz |
Liveness probe (ok); maps check probes it from inside the image |
/metrics |
Prometheus counters: requests, cache hits, renders, render time, geometry tiles and bytes sent, cache size |
The server shuts down gracefully on SIGTERM. Pushes to main publish
ghcr.io/kensac/maps (latest and sha-<short>).
Building the map from an extract briefly needs about twice the memory it
serves with. maps build does that once and writes a snapshot, which every
command accepts in place of the .osm.pbf:
| US Northeast (1.8 GB PBF, 25 M features) | From PBF | From snapshot |
|---|---|---|
| Load time (M1 Pro) | 42 s | 3.5 s |
| Peak memory | 12 GB | 5.7 GB |
Ingest keeps the build lean: node coordinates are written once into a packed 32-bit fixed-point array (about 1 cm), node references become 32-bit indices so the IDs can be freed, raw ways are released as they are built, and the feature sort is in place.
The snapshot is 3.8 GB. Serving it uses about 5.7 GB plus the tile cache.
Measured on an Apple M1 Pro (10 cores), release build.
| Extract | PBF | Features | Load |
|---|---|---|---|
| JFK airport | 2.3 MB | 59 k | 0.12 s |
| Manhattan + Brooklyn | 16 MB | 361 k | 0.34 s |
| New York metro | 184 MB | 3.9 M | 4.5 s, then 1.7 GB resident |
Serving the NYC extract (Midtown, 512 px retina tiles at bearing 29° and pitch 45°, 16 concurrent clients):
| Average latency | Max | |
|---|---|---|
| Uncached tile (rendered on demand, about 16 ms CPU) | 18 ms | 99 ms |
| Cached tile | 0.5 ms | 3 ms |
A tilted 8000 px poster of the whole metro area renders in about 3 s after loading. The previous version of this project took 17 s for a single unstyled 4096 px image.
.osm.pbf ─► ingest ─► map ─────────────────► render ─► server / tiles / poster
3 passes rings, coastlines, ground layers, then a
in parallel parts, roofs placed, depth-sorted 3D scene
R-tree per zoom
Ingest (src/ingest.rs) reads the PBF in three parallel passes over its
compressed blobs:
- Relations.
- Ways, skipping node blobs undecompressed.
- Only the referenced nodes, plus tagged objects, skipping way blobs.
It classifies tags into physical kinds with real or typical dimensions
(src/classify.rs).
Map building (src/map.rs, src/assemble.rs):
- Assembles multipolygon rings and orients them for non-zero filling.
- Turns coastlines into land.
- Drops building outlines that are drawn through their parts, and lifts rooftop objects onto their buildings.
- Sorts features into draw order and indexes them in one R-tree per zoom bucket.
Rendering (src/render/) uses an orthographic camera with bearing and
pitch. The ground is foreshortened by cos(pitch) and heights rise by
sin(pitch); because that map is affine, any rotated, tilted view is still a
regular tile grid.
- Ground layers are projected, simplified, clipped and rasterized with tiny-skia.
- Everything with height goes into a scene (
scene.rs) and is painted back to front. That includes solids with real roof geometry (solids.rs), bespoke object models (models.rs, on a small 3D mesh rasterizer inmesh.rs) and raised line chunks (lines.rs). - The paint order is a pure function of geometry and camera, so tiles agree at their edges.
Geometry tiles (src/vtile.rs) reuse the same styling and 3D models
but emit vertex buffers instead of pixels: earcut fills, mitered line
ribbons with per-vertex height, building meshes with roofs, and instances
of shared model templates. Flat-roofed solids with one outline go out as
prisms (outline, heights, colors, roof triangles) for the client to
extrude. Detail drops with zoom: full solids, walls and pillars from z15,
simplified buildings below it, street objects from z16. The format (GTL3) is documented at the
top of src/vtile.rs; FORMAT is part of the data version, so a format
change never reuses cached tiles. Every cacheable URL ends in .bin or
.png, which CDNs such as Cloudflare cache by default.
Serving (src/server.rs):
- Tokio and axum handle HTTP; rendering runs on the rayon CPU pool.
- A size-bounded moka cache coalesces concurrent requests for the same tile.
- Renders whose clients have gone are skipped, and a semaphore bounds in-flight work.
- Render buffers are reused per thread.
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt
cargo run --release --example census -- nyc.osm.pbf # tag census of an extractMap data © OpenStreetMap contributors, available under the Open Database License.




