---
title: Datenmodell — Buildings, Surfaces, Tilesets, Terrain
summary: DB-Schema-Übersicht und API-zu-Tabelle-Mapping.
---# Datenmodell

Lodapi hat zwei Schema-Layer:

- **`lodapi_meta`** — Cross-BL Metadaten (Datasets, Tilesets, Terrain-Datasets, Lizenzen, Attribution).
- **`bl_<code>`** — Pro Bundesland ein Schema mit Postgres-Tabellen aus dem Import (LoD2-Slim-Variante + ggf. 3DCityDB-v5-Schema im Hintergrund).

## `lodapi_meta` — Plattform-Schicht

```
lodapi_meta.license               (license_id PK, url, text, ...)
lodapi_meta.attribution           (attribution_id PK, bundesland_code, text, ...)
lodapi_meta.dataset               (dataset_id PK uuid, bundesland_code, snapshot_date, ...)
lodapi_meta.tileset               (tileset_id PK uuid, dataset_id FK, region_code, s3_key, bounding_volume, ...)
lodapi_meta.terrain_dataset       (terrain_dataset_id PK, bundesland_code, snapshot_date, source, format, ...)
lodapi_meta.terrain_tile          (tile_id PK, terrain_dataset_id FK, bbox, easting_km, northing_km, source_url, ...)
lodapi_meta.terrain_mesh_dataset  (mesh_dataset_id PK, bundesland_code, snapshot_date, tileset_s3_key, build_params jsonb, ...)
```

| API-Endpoint | Quelltabelle(n) |
|---|---|
| `/v1/datasets` | `dataset` JOIN `attribution` |
| `/v1/tilesets` | `tileset` + spatial-index `bounding_volume` |
| `/v1/tilesets/{id}` | `tileset` |
| `/v1/terrain/datasets` | `terrain_dataset` (DISTINCT ON BL) |
| `/v1/terrain/elevation` | `terrain_tile` (ST_Contains) → `/vsicurl/` |
| `/v1/terrain/profile` | `terrain_tile` × N + batch-COG-sample |
| `/v1/terrain-mesh/datasets` | `terrain_mesh_dataset` (DISTINCT ON BL) |
| `/v1/admin/datasets` (POST) | UPSERT in `dataset` + `tileset` |

## `bl_<code>` — Per-BL-Schicht

Phase-1-Slim-Layout (Decision 2026-05-13, Surfaces-Pivot):

```
bl_<code>.surfaces_slim    (surface_id PK, building_id, gmlid, surface_class, geom MultiPolygonZ, ...)
bl_<code>.dataset_metadata (snapshot_date, building_count, ...)
```

`surface_class` ist die AdV-Klassifikation:
- `709` = Wall (Fassade)
- `710` = Ground (Bodenfläche)
- `712` = Roof (Dach)

| API-Endpoint | Quelltabelle |
|---|---|
| `/v1/buildings` (bbox) | `bl_<code>.surfaces_slim` (DISTINCT building per bbox) |
| `/v1/buildings/{gmlid}` | `bl_<code>.surfaces_slim` GROUP BY building_id |
| `/v1/buildings/3d.glb` | `bl_<code>.surfaces_slim` mit class-Filter |

## Snapshot-Versionierung

Jedes BL hat **ein** aktives Snapshot zu einem Zeitpunkt. Re-Imports überschreiben das Schema in einer Transaktion. `dataset.snapshot_date` ist die Identitätsachse.

Cursor-Pagination in `/v1/buildings` ist **Federation-aware**: `<bundesland>:<surface_id>` als base64-encoded String. Behandle den Cursor als opak — `surface_id` ist nicht als stabile ID über Snapshots hinweg garantiert.

## Index-Strategie

| Tabelle | Index | Zweck |
|---|---|---|
| `surfaces_slim` | GIST(geom) | Bbox-Queries |
| `surfaces_slim` | btree(building_id) | GROUP BY für Detail-Endpoints |
| `surfaces_slim` | btree(gmlid) | `/v1/buildings/{gmlid}` |
| `terrain_tile` | GIST(bbox) | `/v1/terrain/elevation` ST_Contains |
| `tileset` | GIST(bounding_volume) | `/v1/tilesets?bbox=` |

## CRS in der DB

Per-BL unterschiedlich:

| BL-Gruppe | Geometry-CRS |
|---|---|
| BW, BY, HB, HH, HE, NI, NW, RP, SL, SH | EPSG:25832 (UTM32N) |
| BE, BB, MV, SN, ST, TH | EPSG:25833 (UTM33N) |

Die API transformiert dynamisch zu WGS84 für GeoJSON-Output. COG-Sampling-Pfad nutzt das native Tile-CRS.

## Bezug

- [ADR-0003 — CityDB v5 Schema Setup](../../adr/0003-citydb-v5-schema-setup.md)
- [ADR-0006 — API-Layer](../../adr/0006-api-layer.md)
- [ADR-0009 — Terrain-Layer](../../adr/0009-terrain-layer.md)