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;
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the Z/X/Y tile coordinate system?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Z/X/Y stands for zoom level, column, and row. At zoom 0, the entire world fits in one 256×256 pixel tile. Each additional zoom level doubles the tile count in each dimension, so zoom N has 2^N columns and 2^N rows. Tile URLs follow the pattern /tiles/{z}/{x}/{y}.png.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between raster and vector map tiles?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Raster tiles are pre-rendered PNG or JPEG images — simple to serve but large in storage and fixed in style. Vector tiles (MVT/Protobuf) contain raw geometry and attribute data, allowing the client to apply dynamic styles, support high-DPI displays, and enable interactive feature queries at much smaller file sizes.”
}
},
{
“@type”: “Question”,
“name”: “How does a metatile improve map tile rendering efficiency?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A metatile renders an 8×8 block of 64 tiles in a single pass, amortizing the fixed startup cost of the renderer and eliminating border artifacts that occur when adjacent tiles are rendered separately. The resulting image is cropped into 64 individual tile files and uploaded to object storage.”
}
},
{
“@type”: “Question”,
“name”: “How do you invalidate map tiles after a data update?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Compute the bounding box of the changed geographic area. Convert that bounding box to tile coordinates at each affected zoom level. Enqueue re-render jobs for all tiles intersecting the bounding box. Once re-rendered, new tiles overwrite the old files in S3 and CDN cache is purged for those tile paths.”
}
}
]
}
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