Carta — Map Tile Server

Map tiles from raw PBF in pure C

Carta is a map tile server written in C11 with zero graphics dependencies. No Cairo, no Mapnik, no FreeType. It reads OpenStreetMap PBF data directly, builds an R-tree spatial index, and renders PNG raster tiles, MVT vector tiles, and ASCII art tiles — all from the same codebase. Software renderer with anti-aliased lines, label placement, metatile caching, and zoom-adaptive LOD. Compiles to WASM for browser demos.

164 tests 3 output formats Zero deps C11 WASM ~18K LoC ~75ms/tile AGPLv3 + Trucking Exception
Why

A tile server you can deploy anywhere

Standard tile pipelines are complex: PostGIS + Mapnik + Tirex for raster, or osm2pgsql + pg_tileserv for vector. Each requires PostgreSQL, dozens of dependencies, and GB of disk for a simple deployment. Want to run it in a browser? Forget it.

Carta takes a different approach: read PBF, render tiles, serve HTTP. One C binary, one PBF data file, zero external services. The same code renders tiles natively at ~75ms, or runs in the browser via WASM with embedded data. For a trucking fleet, this means map tiles served from the edge — no cloud tile provider needed.

Pure C software renderer
Xiaolin Wu anti-aliased lines, even-odd polygon fill, road casing, label placement with collision detection. No Cairo, no Skia, no GPU required. Renders identically on Linux, macOS, and in the browser via WASM.
Three output formats
PNG raster tiles (512×512), MVT vector tiles (Mapbox spec v2), and ASCII art tiles (4 character sets including Unicode Braille). Same data, same spatial index, three completely different renderers.
Direct PBF parsing
No intermediate database. Load a PBF file, build an R-tree index, start serving. Country-scale data loads in seconds. No PostGIS, no osm2pgsql, no import step. Updates: swap the PBF and restart.
Production-hardened server
Rate limiting, work queue with backpressure, ETag caching (304 Not Modified), adaptive capacity tuning from P50 response times, worker thread pool with SO_REUSEPORT load balancing. TileJSON 3.0.0 metadata.

Edge deployment. Trucks have intermittent connectivity. A Carta binary + country PBF on a vehicle-mounted computer serves map tiles offline. Same binary compiles to WASM for the web dashboard. One renderer, every platform.

Rendering Pipeline

From PBF bytes to pixels

  PBF File (OSM data)
       │
       ▼
  ┌──────────────┐     ┌──────────────┐
  │ ct_pbf.c     │────▶│ ct_rtree.c   │  Hilbert-packed R-tree
  │ Parse nodes, │     │ Spatial      │  O(log n + k) queries
  │ ways, rels   │     │ index        │
  └──────────────┘     └──────┬───────┘
                              │
  GET /tiles/14/8588/5664     │  Tile request
       │                      │
       ▼                      ▼
  ┌──────────────┐     ┌──────────────┐
  │ ct_tile.c    │────▶│ ct_lod.c     │  Level-of-detail
  │ Web Mercator │     │ Zoom-based   │  filtering
  │ projection   │     │ feature rules│
  └──────────────┘     └──────┬───────┘
                              │
                    ┌─────────┼─────────┐
                    ▼         ▼         ▼
             ┌──────────┐ ┌───────┐ ┌───────┐
             │ ct_render │ │ct_mvt │ │ct_ascii│
             │ Rasterize │ │Protobuf│ │ Chars │
             │ → PNG     │ │encode │ │ + ANSI│
             └──────────┘ └───────┘ └───────┘
~75ms
Avg Tile Latency
164
Tests
~18K
Lines of C
<1ms
Cache Hit

Software rasterizer

Carta's rasterizer draws directly to an RGBA pixel buffer. No GPU, no OpenGL, no external graphics library. Anti-aliased lines via Xiaolin Wu's algorithm, polygon fill with even-odd rule, road casing with configurable width.

  • SIMD optimization — 8× speedup for opaque polygon fill
  • Pre-allocated buffers — eliminates 500+ malloc/free pairs per tile
  • Thread-local render contexts — zero contention between workers
  • Miniz compression level 2 — 3× faster than default PNG encoding

Label system

Three types of labels, all with collision detection on an 8×8 grid:

Label Type Algorithm Features
Point labels 9-anchor placement Cities, towns, POIs. Priority: country (100) → village (50)
Road labels Text along path Per-glyph angle, follows road geometry
Area labels Pole of inaccessibility Lakes, parks. Finds interior point farthest from edges

Metatile caching

Labels that span tile boundaries look wrong when each tile is rendered independently. Carta's metatile system renders 2×2 tile groups with a shared collision grid, then extracts individual tiles. Thread-safe LRU cache with pthread_rwlock and refcounted entries. Default capacity: 4,096 metatiles.

Level of detail

Zoom-adaptive feature visibility prevents tile clutter at low zoom levels:

Feature Min Zoom Feature Min Zoom
Motorways z5 Buildings (>5000m²) z13
Primary roads z8 Buildings (all) z15
Residential roads z14 Large lakes z4
Service roads z15 Rivers z8
Output Formats

PNG, MVT, and ASCII from the same data

PNG raster tiles

# Fetch a PNG tile (z14, downtown Budapest)
curl http://localhost:8081/tiles/14/9116/5693.png -o tile.png

# 512x512 pixels, ~75ms generation, <1ms cache hit
# ETag support for 304 Not Modified

MVT vector tiles

# Fetch an MVT tile (Mapbox Vector Tile spec v2)
curl http://localhost:8081/tiles/14/9116/5693.mvt -o tile.mvt

# Layers: background, landuse, water, buildings,
#         roads, railways, boundaries, labels
# Delta + zigzag coordinate encoding
# Douglas-Peucker simplification (20-30% smaller)

ASCII art tiles

# Render as ASCII art
curl "http://localhost:8081/tiles/14/9116/5693.txt?width=80&charset=braille&color=1"

# Charsets: simple (10-level), extended, blocks (Unicode), braille (max detail)
# Options: width, height, invert, ANSI color

TileJSON metadata

# TileJSON 3.0.0 endpoint
curl http://localhost:8081/tiles.json

# Returns: tilejson, tiles URL template, bounds,
#          center, minzoom, maxzoom, attribution
Endpoint Format Content-Type Use Case
/tiles/{z}/{x}/{y}.png PNG raster image/png Web maps, offline maps, previews
/tiles/{z}/{x}/{y}.mvt MVT vector application/vnd.mapbox-vector-tile MapLibre GL, Mapbox GL, custom renderers
/tiles/{z}/{x}/{y}.txt ASCII art text/plain Terminal, monitoring, novelty
/tiles.json TileJSON 3.0.0 application/json Client auto-configuration
/api/v1/health JSON application/json Health check
/api/v1/stats JSON application/json PBF stats, cache stats
Under the Hood

R-tree, LRU cache, worker pool

Spatial index

Hilbert-packed Sort-Tile-Recursive R-tree. O(log n + k) tile queries vs O(n) linear scan. Built once at startup from PBF data. Packed node structure for cache-line efficiency. Carta queries the R-tree for every tile request to find which OSM features intersect the tile bbox.

Tile cache

Per-zoom LRU cache with configurable total size (default 256MB). Separate caches for PNG and MVT. 64-bit key packing: z(5) | x(29) | y(30). Cache hit latency: <1ms vs ~75ms generation. ETag headers use FNV-1a 64-bit hash for HTTP 304 responses.

Server architecture

Component Implementation Configuration
HTTP server Mongoose (event loop) Port 8081
Rate limiter sh_ratelimit (token bucket) 10 RPS, burst 100
Work queue sh_workqueue (bounded) 256 depth, 5s timeout
Workers sh_worker_pool Auto CPU count
Adaptive sh_adaptive (P50/P90/P99) Optional, 70% target
CORS sh_cors Origin allowlist

Codebase

~18K
Lines of C
164
Tests
21
Headers
27
Source Files

Honest assessment. Carta is not Mapnik. Styling is not configurable via CartoCSS or MapboxGL JSON — road colors and widths are compiled in. Label rendering uses an embedded bitmap font, not FreeType with TTF fonts. For a fleet operations dashboard showing truck positions on a map, this is fine. For a consumer map product competing with Google Maps, you'd want Mapbox GL or MapLibre GL on top of Carta's MVT tiles.

Quick Start

Three ways to try Carta

1. Browser (zero install)

The API documentation page includes a live WASM demo with embedded Monaco data. Click to generate PNG, MVT, or ASCII tiles. No server needed.

2. Build from source

# Clone and build
git clone https://github.com/ottofleet/otto.git
cd otto && make carta

# Run all 164 tests
make test-carta

3. Tile server

# Build and start server
make carta-api
./carta/api/carta-tile-server data/hungary-latest.osm.pbf

# Fetch a PNG tile
curl http://localhost:8081/tiles/14/9116/5693.png -o tile.png

# Fetch an MVT tile
curl http://localhost:8081/tiles/14/9116/5693.mvt -o tile.mvt

# ASCII art in your terminal
curl "http://localhost:8081/tiles/14/9116/5693.txt?width=80&color=1"
FAQ

Common questions

Why build a tile server from scratch?

Three reasons: (1) Edge deployment — one binary + one PBF file, no PostgreSQL, no import pipeline. Put it on a truck's onboard computer for offline maps. (2) WASM demos — the same C code renders tiles in the browser. (3) Zero dependencies — no Cairo, no Mapnik, no transitive dependency chain. What you audit is what you ship.

How does Carta compare to Mapnik or MapServer?

Mapnik is far more capable at cartography: configurable styling, TTF fonts, SVG symbols, labeling heuristics refined over 15 years. Carta is simpler and self-contained. The tradeoff is deployment complexity (Mapnik needs PostGIS + dozens of libs) vs. rendering quality (Carta has fixed styling). For fleet dashboards showing vehicle positions and route overlays, Carta's quality is sufficient. For consumer map products, use Carta's MVT output with MapLibre GL for rendering.

What about vector tile rendering in the browser?

Carta's MVT tiles are fully compatible with MapLibre GL and Mapbox GL. Use Carta as a vector tile server and let the browser handle beautiful rendering with WebGL. The PNG output is for contexts where client-side rendering isn't available (email reports, server-generated PDFs, terminal dashboards).

How large are the PBF files?

Monaco: 450KB. Hungary: 400MB. Germany: 3.5GB. Planet: 72GB. Carta loads data into memory (10–15× PBF size for peak RAM), so country-scale works on a standard server. Continental-scale needs 64–128GB RAM or sharding. See the roadmap for planned continental sharding support.

What license?

AGPLv3 with a Trucking Exception. Same terms as all OTTO modules.