Home -> Examples -> eu5

OGC WMS/WMTS/TMS · live PostGIS

Five countries, one live database, the EU's own grid.

Every other demo here reads a file. This one reads a database: a 63 GB PostGIS table holding 154.8 million OpenStreetMap features across Germany, France, the Netherlands, Belgium and Luxembourg, queried live, per tile. It also runs on the grid the EU itself publishes for pan-European data, EuropeanETRS89_LAEAQuad (EPSG:3035, INSPIRE's own equal-area grid), not Web Mercator. The zoom levels most people actually browse are precomputed archives, read in about a millisecond; zoom in past them and you're watching a live spatial query answer instead.

eu5 Buildings, roads and water across a swath of Germany, rendered from PostGIS on the EU's own LAEA grid, in the TerraServe X-ray viewer

The approach

A live database with the archive's speed.

The EU's own grid

Not Web Mercator: EuropeanETRS89_LAEAQuad

The swiss demo proved TerraServe can serve any tile grid. This one does it at EU scale, on the grid INSPIRE actually specifies for pan-European datasets: EPSG:3035, a Lambert azimuthal equal-area projection with its own origin and cell sizes, published as one OGC TileMatrixSet document the viewer reads directly, so raster and vector tiles can't drift out of registration with each other.

Live, not static

107.9 million buildings, queried per tile

Every other example on this site opens a file. This one queries PostGIS: for a given tile, only the rows inside that tile's bbox are ever fetched, so a request for one Frankfurt block never touches the other 154.7 million features. The same windowed-read discipline as the vida demo, just against a live relational database instead of a flat file.

Precomputed where it matters

z0-10 is a read, not a query

The zoom range most visitors actually use, continent down to city, is pre-baked to tile archives for all five layers, both as vector tiles and as ready-made raster images. A z9 tile from the archive has measured at about 1.1 ms. Zoom in past z10 and PostGIS answers directly, live, still scoped to just the tile in view.

The honest part

A 1-pixel gate erased every building at the edge of the map.

The buildings archive was baked with the same size gate every other layer uses: skip shapes smaller than a set number of pixels, so overview zooms aren't crowded with dust. At most zooms, on most datasets, that's invisible. At this grid's zoom 0, a pixel covers about 17.5 km on a side, so a gate of even a single pixel demands a building bigger than 309 km² to survive. No building on Earth clears that bar. The bake didn't thin the buildings layer at the shallowest zooms, it wrote zero tiles for it, silently, and a visitor who zoomed all the way out got a live query against 107.9 million rows instead, which is exactly the request that used to time out.

Fixed by re-baking buildings ungated for its full z0-10 range, the same choice its own raster archive had already made for the same reason. The lesson generalizes past this one layer: any threshold tuned for a comfortable middle zoom can silently produce nothing, not just less, at the far end of the range. Check the shallowest zoom explicitly, every time.

eu5 · buildings by class Individual building footprints colour-coded by class, industrial, commercial, school, hospital, along a river in Frankfurt
Six classes, one style file

Buildings colour-coded by use

Industrial, commercial, retail, warehouse, school and hospital each get their own colour from a single MapLibre style file. That file is global to the process, not per layer, so water/roads/landuse/places on this demo fall through to its default colour rather than being individually themed, a real, disclosed limitation of a one-style-file-per-process design.

Archive vs live, side by side

Toggle the WMTS layer to see it

The X-ray viewer's raster underlay reads through /wmts, which serves the same z0-10 archive as the vector tiles, so switching it on costs nothing extra inside the archived range. Past z10 it renders live from PostGIS, same as the vector layer above it, so the two never fall out of sync.

How it's served

The exact command.

One process, five layers, one YAML config. Most of the flags below exist because of the bug above.

terraserve serve # 154.8M OSM features, five layers, one config
  --config /data/eu5/eu5.yaml            # buildings, roads, water, landuse, places
  --mvt-style /data/eu5/eu5.mvt-style.json
  --mvt-min-feature-px 1.0  --raster-min-feature-px 0.05  # ~40x apart, on purpose
  --mvt-cache 512  --wms-cache 512  --max-inflight 8
  --public-url https://terraserve.io/demo/eu5
--config eu5.yaml Five layers in one YAML file: buildings, roads, water, landuse, places. Each has its own PostGIS table, its own grid list, and its own precomputed archive paths. One process serves all five.
grids: [EuropeanETRS89_LAEAQuad.json] The OGC-registered EU grid, EPSG:3035, written out as one TileMatrixSet JSON file, the same mechanism the swiss demo uses for its national grid, here at continental scale instead.
--mvt-style eu5.mvt-style.json A match() expression keyed on the building tag: industrial, commercial, retail, warehouse, school and hospital each get a colour. --mvt-style is global per process, not per layer, so it also governs water/roads/landuse/places on this demo, and they fall through to its default colour.
--mvt-min-feature-px 1.0 The cartographic gate: skip shapes under about 295 m² at this grid's zoom 10. Tuned down from a 2.0 default that was erasing roughly half of every building footprint at LAEA's resolution.
--raster-min-feature-px 0.05 The safety gate for the raster path, deliberately about 40× smaller than the MVT one. Still bounds a worst-case request to 0.07 GiB instead of 52.6 GiB, without visibly thinning the map.
TERRASERVE_PG_STATEMENT_TIMEOUT_MS=90000 Raised 3× from Postgres's own 30 s default, after a live request over nearly a whole country outlasted it. A safety margin for an occasional wide request, not a promise every bbox now succeeds.
TERRASERVE_PG_MAX_QUERY_FEATURES=150000000 The correctness knob a table this large needs: the 500,000 default would silently truncate a 107.9-million-row table with no ORDER BY, so two identical requests could return different tiles.

Measured on the live server: a z9 tile from the buildings archive returns in about 1.1 ms. Archive coverage across all five layers is now gap-free from z0 to z10, the buildings archive originally shipped without z0/z1 at all, for the reason above, and that was found and fixed the same day it went live.

Source & attribution. OpenStreetMap data for Germany, France, the Netherlands, Belgium and Luxembourg, extracts via Geofabrik, © OpenStreetMap contributors, ODbL. Tile grid: EuropeanETRS89_LAEAQuad (EPSG:3035), the equal-area Lambert azimuthal grid the EU publishes for INSPIRE data, written as an OGC TileMatrixSet.