Tile Coordinate System
Map tiles use a Z/X/Y (zoom / column / row) scheme:
- Zoom 0 = 1 tile covering the entire world
- Zoom 1 = 4 tiles (2×2 grid)
- Zoom N = 2^N x 2^N tiles
- Zoom 18 = 68 billion tiles — only generated on demand
tile_count(zoom) = 2^zoom * 2^zoom
tile_url = /tiles/{z}/{x}/{y}.png
Tile Formats
- Raster (PNG/JPEG) — pre-rendered images; simple to serve, large storage footprint
- Vector (MVT/Protobuf) — raw geometry data; dynamic client-side styling, smaller payloads
Tile Generation Pipeline
OpenStreetMap data (PBF)
-> osm2pgsql -> PostGIS database
-> Tile Renderer
Raster: Mapnik -> PNG tiles
Vector: Tegola -> MVT tiles
-> Object Storage (S3): tiles/{z}/{x}/{y}.png
-> CDN (CloudFront / Fastly)
-> Client
Storage Layout
s3://map-tiles-bucket/
raster/
0/0/0.png
1/0/0.png
...
18/x/y.png
vector/
0/0/0.mvt
...
-- Optional metadata table
CREATE TABLE tile_metadata (
zoom SMALLINT,
x INT,
y INT,
generated_at TIMESTAMP,
size_bytes INT,
PRIMARY KEY (zoom, x, y)
);
CDN Delivery
- Tiles are served from CDN edge nodes; S3 is the origin
- Tiles are immutable for a given zoom+coordinate once rendered
- Set
Cache-Control: public, max-age=31536000, immutablefor indefinite CDN caching - Low origin hit rate after warmup — CDN absorbs >95% of traffic
Metatile Rendering
Render an 8×8 block (metatile = 64 tiles) in a single pass to amortize renderer startup cost and eliminate seam artifacts at tile borders. Split result into 64 individual tile files before uploading to S3.
metatile_render(z, meta_x, meta_y):
image = render_8x8_block(z, meta_x*8, meta_y*8)
for dx in 0..7:
for dy in 0..7:
tile = crop(image, dx*256, dy*256, 256, 256)
s3_put('tiles/{z}/{meta_x*8+dx}/{meta_y*8+dy}.png', tile)
Pre-generation vs On-demand
- Zoom 0-14: pre-generate all tiles offline during initial build and after map data updates
- Zoom 15-18: render on-demand per request, cache result in S3/CDN immediately
Tile Invalidation
When map data is updated, re-render only tiles intersecting the changed geographic bounding box:
changed_bbox = (min_lng, min_lat, max_lng, max_lat)
for zoom in 0..14:
x_min, y_min = lng_lat_to_tile(changed_bbox.min_lng, changed_bbox.max_lat, zoom)
x_max, y_max = lng_lat_to_tile(changed_bbox.max_lng, changed_bbox.min_lat, zoom)
enqueue_render_jobs(zoom, x_min..x_max, y_min..y_max)
Rate Limiting
Apply per-API-key rate limiting at the CDN or API gateway layer to prevent bulk tile scraping. Unauthenticated requests get a lower tier limit.
-- Nginx rate limit example
limit_req_zone zone=tiles:10m rate=500r/m;
limit_req zone=tiles burst=100 nodelay;
See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering