Estimates building volumes and gross floor areas using publicly available Swiss elevation models and cadastral data.
The solution is available in three variants:
- Web App — Zero-install browser app. Entry point is
index.htmlat the project root (so GitHub Pages picks it up natively); CSS and JS modules live inwebapp/. Upload a CSV with building EGIDs, get volumes and floor areas on a map with export to CSV/Excel/GeoJSON. - Python CLI — Open-source, requires Python 3.10+ and free dependencies. Processes locally with exact LV95 areas and local elevation tiles. See python/README.md for full CLI reference, output schema, and developer details.
- FME — Workbench implementing Steps 1–3, requires a licensed copy of FME Form. See fme/README.md for the transformer pipeline.
The browser-based version runs entirely client-side — no backend, no installation. Upload a CSV with id and egid columns and the app will:
- Fetch building footprints from geodienste.ch WFS (
ms:LCSF, filtered byGWR_EGID), with automatic swisstopo vec25 fallback for cantons not on geodienste.ch - Load elevation data (DTM + DSM) from swisstopo COG tiles on-the-fly
- Compute volume and heights using an orientation-aligned 2×2m grid
- Look up building classification from GWR via swisstopo API
- Estimate floor areas from building type and volume
- Display results on an interactive map with table and summary panel
- Interactive Map — MapLibre GL JS with 3D building extrusions, orientation-aligned grid cell visualization, 4 basemaps, scale bar
- Layer Panel — Toggle Gebäudegrundflächen, Gebäudevolumen, Rasterzellen, Beschriftungen, and AV cadastral overlay
- Summary Panel — Collapsible sections for building status, volume/height aggregates, and floor area estimates
- Table Widget — Sortable columns, search filter, pagination, resizable panel, row click → map highlight
- Export — CSV, Excel (XLSX), and GeoJSON with timestamped filenames
- Privacy — All data stays in the browser. Only EGID and coordinates are sent to public APIs
| Web App | Python CLI | |
|---|---|---|
| Data coverage | All 26 cantons — WFS for most cantons, automatic vec25 fallback for blocked cantons (JU, LU, VD) | All cantons via local GeoPackage or --use-api (same cascade) |
| Elevation data | On-the-fly COG tile loading from swisstopo CDN | Local GeoTIFF tiles (faster, offline) |
| Grid resolution | 2×2m (configurable) | 1×1m (configurable) |
| Area calculation | Spherical (Turf.js), ~0.1–0.5% error for spatial matching | Exact planar (LV95/EPSG:2056) |
| Throughput | ~5 buildings in parallel, limited by API rate | Bulk processing with local data |
| EGID lookup | Direct WFS filter by GWR_EGID |
Local GeoPackage spatial join or API cascade |
| Offline | Requires internet | Fully offline with local data (or API mode with internet) |
Data coverage note: Most cantons are freely available via the geodienste.ch WFS. Three cantons (JU, LU, VD) don't publish data there, so both the Web App and
--use-apimode automatically fall back to swisstopo vec25 building footprints (lower accuracy, ~2-year update cycle). Coverage can also be incomplete in TI and VS.
Open index.html in a browser (requires a local server for ES modules):
cd area-estimator
python -m http.server 8080
# Open http://localhost:8080Or deploy to any static hosting (GitHub Pages, Cloudflare Pages, etc.). GitHub Pages serves the root index.html directly with no special configuration — the asset references inside index.html point at webapp/css/, webapp/js/, and data/example.csv, all of which are siblings of index.html in the served tree.
Why this layout? GitHub Pages' "Deploy from a branch" mode only accepts the branch root or a
/docsfolder as the publishing source — arbitrary subfolders likewebapp/aren't supported. Keeping a thinindex.htmlat the root means GitHub Pages works out of the box, while the technical assets (CSS, JS) stay inwebapp/to keep the repo root clean.
| API | Purpose | Auth |
|---|---|---|
geodienste.ch/db/av_0/{lang} WFS |
Building footprints by EGID or BBOX (ms:LCSF) |
None (CORS) |
api3.geo.admin.ch/MapServer/find |
GWR building attributes + coordinates by EGID | None (CORS) |
api3.geo.admin.ch/MapServer/identify |
vec25 building footprints — fallback for blocked cantons | None (CORS) |
data.geo.admin.ch |
swissALTI3D + swissSURFACE3D COG tiles | None (CORS) |
flowchart TD
subgraph INPUT["Inputs"]
A1A["⚪ --footprints<br>AV GeoPackage / Shapefile / GeoJSON<br><i>required unless --use-api</i>"]
A1B["⚪ --csv<br>CSV: id, egid (default)<br>or id, lon, lat (with --use-coordinates)<br><i>optional (required with --use-api)</i>"]
A2["🔴 --alti3d<br>swissALTI3D tiles — terrain DTM 0.5m<br><i>required</i>"]
A3["🔴 --surface3d<br>swissSURFACE3D tiles — surface DSM 0.5m<br><i>required</i>"]
end
A1A --> S1
A1B -.->|"--csv"| S1
A2 --> S3
A3 --> S3
S1["<b>Step 1 — Read Footprints</b><br>Mode A: all AV buildings<br>Mode B: EGID match against GWR_EGID (default)<br>Mode C: lon/lat spatial join (--use-coordinates)<br>Mode D: API cascade GWR→WFS→vec25 (--use-api)<br>Unmatched → status: no_footprint"]
S2["<b>Step 2 — Aligned Grid</b><br>Minimum rotated rectangle orientation<br>Grid points filtered to footprint"]
S3["<b>Step 3 — Volume & Heights</b><br>Sample DTM + DSM at each point<br>Volume = Σ max(surface_i − min(terrain), 0) × cell_area"]
S4["<b>Step 4 — Floor Areas</b><br>GWR classification → floor height<br>Floors = height_minimal / floor_height<br>GFA = footprint × floors"]
S1 --> S2 --> S3
S3 --> OUT
S3 -.->|"--estimate-area"| S4
S4 -.-> OUT
OUT[/"<b>Output CSV</b><br>volume · heights · elevations<br>+ floor areas if --estimate-area"/]
subgraph GWR["⚪ GWR Data — optional, only with --estimate-area"]
G1["--gwr-csv<br>CSV bulk download<br><i>housing-stat.ch</i>"]
G2["(default)<br>swisstopo API<br><i>per EGID, live lookup</i>"]
end
GWR -.-> S4
classDef required fill:#fde8e8,stroke:#e02424,stroke-width:2px
classDef optional fill:#f3f4f6,stroke:#9ca3af,stroke-width:1px
classDef step fill:#eff6ff,stroke:#3b82f6,stroke-width:1.5px
classDef optionalStep fill:#f3f4f6,stroke:#9ca3af,stroke-width:1px,stroke-dasharray:4 3
class A2,A3 required
class A1A,A1B optional
class S1,S2,S3 step
class S4 optionalStep
Note: The flowchart describes the Python CLI pipeline. With
--use-api, Step 1 fetches footprints from public APIs instead of a local file (GWR → geodienste.ch WFS → swisstopo vec25 cascade). The Web App follows the same 4 steps and the same API cascade. The FME workbench implements Steps 1–3 only (no floor-area estimation).
pip install -r python/requirements.txt
# With a local AV GeoPackage (fastest, offline-capable)
python python/main.py \
--footprints "D:\AV_lv95\av_2056.gpkg" \
--csv data/example.csv \
--alti3d "D:\SwissAlti3D" \
--surface3d "D:\swissSURFACE3D Raster" \
--estimate-area \
-o portfolio_volumes.csv
# Or via API — no local AV file needed (requires internet)
python python/main.py \
--csv data/example.csv \
--use-api \
--alti3d "D:\SwissAlti3D" \
--surface3d "D:\swissSURFACE3D Raster" \
--estimate-area \
-o portfolio_volumes.csvFor the full command-line reference, output schema, accuracy bucketing, warning catalog, and Floor Height Lookup table, see python/README.md.
The pipeline uses two distinct Swiss data registers, linked by the GWR_EGID attribute:
- AV (Amtliche Vermessung) — the cadastral survey, providing building geometry (parcel and footprint polygons). Maintained by cantonal survey offices, available via geodienste.ch.
- GWR (Gebäude- und Wohnungsregister) — the federal building register, providing building master data: addresses, classification, construction year, dwelling counts. Maintained by the Federal Statistical Office, available via the BFS and swisstopo APIs.
The pipeline uses AV polygons for the footprint geometry (needed by Steps 1–3 to compute volume) and GWR classification (needed by Step 4 to convert volume to floor area). EGID is the natural key. A few percent of AV building polygons have no EGID assigned, which is why coordinate-based matching is kept as an option (--use-coordinates).
| Limitation | Detail |
|---|---|
| No underground estimation | LIDAR only sees above ground — basements and underground floors are not included |
| Trees over buildings | The surface model doesn't distinguish roofs from foliage — tall trees over small buildings inflate the measured height and volume |
| Surface model merging | swissSURFACE3D combines ground, vegetation, and buildings into one surface; this can cause overestimation near vegetation |
| Small buildings | Footprints smaller than the grid cell size produce no grid points and can't be measured |
| Mixed-use buildings | A single floor height is applied per building; actual floor heights may vary (e.g. retail ground floor + residential upper floors) |
| Industrial / special buildings | Floor height ranges are wide (4–7 m), so floor count estimates are less reliable |
| Data timing | The elevation model may have been captured before or after the building was constructed or modified |
| Sloped terrain | Volume is measured from elevation_base_min (lowest terrain point) as a flat datum. On steeply sloped sites, this includes terrain undulation. |
| Polygon validity vs. display | A handful of AV polygons have edge-case geometry (self-touching rings, near-degenerate vertices) that some GIS renderers (e.g. ArcGIS) refuse to draw. The planar area is still computed correctly — Shapely/GEOS is more permissive about edge-case validity than display engines, and polygon.area returns the right value for these features. If you need to display the same polygons in a map, you may need to dissolve/clean them in your GIS tool first; that does not affect this pipeline's numbers. |
| vec25 fallback accuracy | In cantons not on geodienste.ch (JU, LU, VD), both the Web App and --use-api mode use swisstopo vec25 footprints, which have a ~2-year update cycle and lower geometric accuracy than official AV data |
area-estimator/
├── README.md ← You are here (overview, web app, limitations)
├── index.html ← Web app entry point (served by GitHub Pages)
├── webapp/ ← Web app technical assets
│ ├── css/ Stylesheets (tokens.css, styles.css)
│ └── js/ Modules (main.js, processor.js, …)
├── python/ ← Python CLI — see python/README.md
│ ├── README.md CLI reference, output schema, tests, type hints
│ ├── main.py CLI entry point + aggregate_by_input_id
│ ├── footprints.py Step 1: load footprints (AV file / API cascade)
│ ├── volume.py Steps 2 + 3: grid + volume + heights, BuildingResult
│ ├── area.py Step 4: GWR enrichment + floor area
│ ├── tile_fetcher.py On-demand tile download
│ ├── tests/ pytest suite — see python/tests/README.md
│ └── requirements.txt
├── experimental/ ← Standalone exploration tools (not in main pipeline)
│ ├── mesh-builder/ Watertight 3D building hulls + viewer ★
│ ├── roof-shape-from-buildings3d/ Roof shape from swissBUILDINGS3D 3D meshes
│ ├── green-roof-from-rs/ Green roof coverage via NDVI on swissIMAGE-RS
│ └── floor-level-estimator/ Earlier per-floor estimator with gbaup factor
├── fme/ ← FME workbenches — see fme/README.md
│ ├── README.md Volume Estimator pipeline summary
│ ├── Volume Estimator FME.fmw The main workbench (Steps 1–3)
│ └── experimental/ Older / unmaintained workbenches
│ ├── green-roof-eval/ Green roof detection from swissIMAGE RS
│ └── roof-estimator-deprecated/ Earlier FME version of the roof estimator
├── docs/
│ ├── Height Assumptions.md Validation study for the floor-height table
│ └── 20260112_GruenflaechenInventar.pdf Reference document for the green-roof tool
├── legacy/ ← Original implementations (historical reference, untouched)
├── data/ ← .gitignored except example.csv
│ └── example.csv Demo data for both web app and Python CLI
└── assets/ ← Images used by the READMEs and the web app
Self-contained tools that aren't part of the main pipeline live in experimental/. Each is independently runnable and has its own README.
| Tool | Status | What it does |
|---|---|---|
| experimental/mesh-builder/ | working | Watertight 3D building hulls from AV cadastral footprints + swisstopo DSM/DTM rasters, with an in-browser three.js viewer |
| experimental/roof-shape-from-buildings3d/ | working | Per-building roof characteristics (area, slope, shape, height) by analysing swissBUILDINGS3D 3D mesh geometry |
| experimental/green-roof-from-rs/ | working | Per-building green roof coverage via NDVI on swissIMAGE-RS multispectral imagery |
| experimental/floor-level-estimator/ | unmaintained | Earlier per-floor estimator with construction-period (gbaup) factor |
| Feature | Description |
|---|---|
| Roof geometry estimation | Classify roof shapes (flat, gable, hip) and estimate roof surface areas |
| Outer wall quantities | Estimate exterior wall areas from footprint perimeter and height metrics |
| Material classification | Building material detection from imagery or other data sources |
| International buildings | Extend beyond Switzerland using alternative elevation and cadastral data |
| Eaves-height floor count | Use elevation_roof_min − elevation_base_min (≈ eaves height for pitched roofs) as the input to floor counting instead of height_minimal. Equivalent for flat roofs, more accurate for SFH/MFH with attics: height_minimal sits between eaves and ridge and slightly over-counts floors. Cheap to add as an extra height_eaves_m column in Step 3. |
| Voxel-slice GFA estimation | Replace footprint × floors with horizontal slab integration over the per-cell heightfield: for each slab k, count cells where building height ≥ slab ceiling, multiply by cell area, sum across slabs. Naturally handles setbacks, attics, towers, dormers, and stepped buildings — cases where the current method silently overcounts because it assumes every floor is the full footprint. Open questions to investigate: (1) cell-qualification rule (strict vs. centerline vs. tunable threshold for partial floors), (2) sensitivity to the assumed floor height, (3) handling of trees-over-buildings noise, (4) per-floor slab areas in the output as a JSON column. Should be opt-in via --gfa-method slice and validated against buildings with known drawings before becoming the default. |
| Resource | Link |
|---|---|
| Amtliche Vermessung (AV) | geodienste.ch/services/av |
| swissALTI3D | swisstopo.admin.ch |
| swissSURFACE3D Raster | swisstopo.admin.ch |
| swisstopo Search API | docs.geo.admin.ch |
| swisstopo Find API | docs.geo.admin.ch |
| GWR | housing-stat.ch |
| GWR Public Data | housing-stat.ch/data |
| GWR Catalog v4.3 | housing-stat.ch/catalog |
| Canton Zurich Methodology | Seiler & Seiler GmbH, Dec 2020 — are.zh.ch |
| DM.01-AV-CH Data Model | cadastre-manual.admin.ch |
| Height assumptions validation study | docs/Height Assumptions.md |
| Library | Version | Purpose |
|---|---|---|
| MapLibre GL JS | 4.7 | Interactive map with 3D fill-extrusion rendering |
| GeoTIFF.js | 2.1 | Cloud Optimized GeoTIFF (COG) reading in-browser |
| Turf.js | 7 | Spatial operations (point-in-polygon, distance, centroid) |
| proj4js | 2.12 | Coordinate transforms (WGS84 ↔ LV95/EPSG:2056) |
| SheetJS (XLSX) | 0.18 | Excel import/export (lazy-loaded) |
| Source Sans 3 | — | Typography |
| Material Symbols | — | UI icons |
| Library | Purpose |
|---|---|
| GeoPandas | Vector geodata processing |
| Rasterio | GeoTIFF reading with windowed access |
| Shapely | Geometry operations, minimum rotated rectangle |
| NumPy | Vectorized grid creation and elevation sampling |
| pyproj | CRS transforms |
| Provider | Dataset | Usage |
|---|---|---|
| swisstopo | swissALTI3D, swissSURFACE3D | Terrain (DTM) and surface (DSM) elevation models at 0.5m resolution |
| swisstopo | vec25 Gebäude (via MapServer identify) | Building footprints fallback for cantons not on geodienste.ch |
| geodienste.ch | Amtliche Vermessung (AV) WFS | Building footprints from official cadastral survey |
| BFS | GWR (Gebäude- und Wohnungsregister) | Building classification, construction year, floor count |
| CARTO | Positron, Dark Matter | Basemap tiles |
Floor area estimation is based on the methodology developed by Seiler & Seiler GmbH (Dec 2020) for the Canton of Zurich ARE. The per-GWR-code accuracy buckets are derived from an independent validation against Swiss regulatory anchors (ArGV4, SIA 2024, cantonal building codes) — see docs/Height Assumptions.md.
MIT License — see LICENSE.



