Update: the new
--source apipath (processor_web.py+api.py+geom_wgs84.py+export_web.py) already achieves web parity — it portsweb/js/processor.jsstep-for-step (synthetic-AV fallback, "Ohne Bauzone" remainder, habitat gap-fill, allcheck_*flags) and exports GeoJSON + Excel.compare.pycross-checks it against a web export. The tasks below remain only for the offline--source gpkgpath, which still reads a local GeoPackage and has not been brought to parity.
Bring the Python GeoPackage pipeline to parity with the web app's overlay
data-quality handling. The web app (web/js/processor.js) is the reference
implementation. Goal: web and Python produce matching per-parcel results, with
every wall-to-wall layer summing to the parcel area — the QS invariant Σ AV land
cover ≈ Σ BAFU habitat ≈ Σ Bauzonen ≈ parcel area.
Both pipelines fetch the two overlay layers (BAFU habitat, ARE Bauzonen) from the geo.admin.ch Identify API, which has two limits that bite:
- Null geometry for oversized features. geo.admin.ch returns
geometry: nullfor features whose geometry exceeds a server-side size cap — e.g. the city-scale9.3.2 Asphalt- und Betonstrassehabitat polygon. Both pipelines silently drop null-geometry features (swisstopo.py_fetch_and_parse, ~line 204:if geom_dict is None: continue), so habitat under-counts andΣ habitat < parcel area. - 200-feature cap + latency. Max 200 features/request; the BAFU habitat layer is also slow (~0.3–1 s/parcel, multi-second spikes) — the main timeout risk.
The web app already works around these (per-parcel habitat gap-fill + an "Ohne Bauzone" remainder). The Python pipeline does not yet.
ch.bafu.lebensraumkarte-schweiz is available via the file-based STAC API
(https://data.geo.admin.ch/api/stac/v1/collections/ch.bafu.lebensraumkarte-schweiz
→ HTTP 200, downloadable zip), which geo.admin.ch recommends for bulk use.
Downloading the layer and intersecting locally solves three problems at once:
no per-parcel latency/timeout, no 200-feature cap, and full geometry (no
null-geometry drop → no gap-fill needed). Python already reads parcels from a
local GeoPackage, so a local habitat layer fits the architecture.
- Confirm the STAC asset format (vector polygons vs. raster). If raster, use zonal statistics (rasterio/rasterstats) per parcel; if vector, intersect like the AV layer.
- Wire it as the habitat source (download/cache the tiles covering the
parcels), replacing the
fetch_habitat_*Identify calls. - Result: habitat sums to the parcel area with no gap-fill.
Fallback if STAC is impractical: mirror the web gap-fill — capture the dropped null-geometry features and attribute the parcel's uncovered area to the dropped type. Wrinkle: Python fetches habitat per municipality (convex hull), so the dropped-feature → parcel association is coarser than the web app's per-parcel Identify.
ch.are.bauzonen is not on STAC (404), but Identify is fast (~10 ms), so keep
it. Mirror processor.js: after clipping zones, emit an Ohne Bauzone detail row
= parcel − covered (exact area) with best-effort geometry (parcel − union(zones)), so Σ Bauzonen = parcel area.
- Emit the
bauzonen_ohne_bauzone_m2column / detail feature. - Guard with a quality flag when a zone was dropped (null geometry) or the result was truncated (≥ 200) — don't let a missing zone pass as zone-free.
- Add
check_habitat/check_bauzonenstatuses matching the web app (ok/estimated/partial/truncated/error).
- Spot-check that web and Python produce matching per-parcel areas and that each wall-to-wall layer sums to the parcel area. Worked example (CH813590469881): habitat and AV both 100 %; Bauzonen 95 % zones + 5 % Ohne Bauzone.
- Web implementation:
web/js/processor.js—fetchIdentify(dropped-feature handling),makeHabitatRow(habitat gap-fill),ohneBauzoneGeometry/makeBauzoneRow(Ohne Bauzone). - Limitations summary: root
README.md→ Known limitations.