# Austrian Landscape Analysis API — v5.4 # Base: https://srtm-lidar-at.exe.xyz:8000 # Geometry: WGS84 (lon/lat). Processing: EPSG:3035. Max area: 25 km². Austria only. ## Overview Watershed segmentation + Random Forest classifier over 6 remote sensing layers. Classifies landscape into 25 object types + 11 group types. Focused on distinguishing man-made vs natural and detecting terrain modification. ## Data Sources (8) | # | Source | Resolution | Access | Role | |---|--------|-----------|--------|------| | 1 | BEV ALS DTM+DSM | 1m | HTTP range /vsicurl/ | Height model, terrain, temporal change (3 dates: 2022/2023/2024) | | 2 | BEV DOP RGBI Orthophoto | 0.2m | HTTP range /vsicurl/ | NDVI, spectral bands, GLCM texture (47 operates + DOP fallback) | | 3 | Sentinel-2 NDVI | 10m | openEO (openeo.dataspace.copernicus.eu) | Growing-season NDVI composite + monthly time series → phenology | | 4 | ESA WorldCover v200 | 10m | AWS COG (esa-worldcover.s3.eu-central-1.amazonaws.com) | Land cover prior (built/tree/crop/grass/water fractions) | | 5 | Sentinel-1 SAR | 10m | openEO | VV+VH backscatter (cloud-proof ground classification) | | 6 | Hansen GFC-2024-v1.12 | 30m | HTTP range (storage.googleapis.com) | Forest cover 2000, annual loss/gain 2001–2024 | | 7 | Austrian Cadastre | mm | REST API (cadastre-process-api.exe.xyz) | Building footprints, parcel boundaries, land-use codes (ground truth) | | 8 | OpenStreetMap | varies | Overpass API | Roads, paths, waterways, landcover (supplementary ground truth) | BEV LiDAR URLs: data.bev.gv.at/download/ALS/{DTM,DSM}/{20220915,20230915,20240915}/ BEV Ortho URLs: data.bev.gv.at/download/DOP/{20220128,20221027,20240625,20250415}/ Hansen URL: storage.googleapis.com/earthenginepartners-hansen/GFC-2024-v1.12/ All raster data accessed via GDAL /vsicurl/ (HTTP range requests) — no bulk downloads. ## Licence & Attribution (READ before redistributing any output) Machine-readable: GET /api/v1/attribution (JSON) or ?format=text. Also embedded in every KG JSON (`attribution` key), every GPKG (`gpkg_metadata` table, dataset scope + per-layer ODbL flag) and every Zenodo deposit description/notes. Single source of truth: attributions.py. Our derivatives (API responses, Zenodo dumps, GPKG/GeoJSON exports) are **CC BY 4.0** (https://creativecommons.org/licenses/by/4.0/). When you re-use them you must carry forward these credits: | Source | Licence | Required credit | |---|---|---| | BEV ALS DTM/DSM, DOP orthophoto, Kataster | CC BY 4.0 (Open Government Data) | "Datenquelle: BEV – Bundesamt für Eich- und Vermessungswesen, , Stichtag , CC BY 4.0, bearbeitet" | | Copernicus Sentinel-1 / Sentinel-2 | Free, full & open (EU Reg. 377/2014; Del. Reg. 1159/2013) | "Contains modified Copernicus Sentinel data " | | ESA WorldCover 2021 v200 | CC BY 4.0 | "© ESA WorldCover project 2021" + Zanaga et al. 2022, doi:10.5281/zenodo.7254221 | | Hansen GFC-2024-v1.12 | CC BY 4.0 | "Hansen/UMD/Google/USGS/NASA" + Hansen et al. 2013, Science 342:850–853 | | OpenStreetMap (roads/paths/water/landcover, power polygons) | **ODbL 1.0** (share-alike) | "© OpenStreetMap contributors" | | austria-power API (Austro Control, IG Windkraft, OSM) | mixed; per-feature `source` | "Austro Control, IG Windkraft, © OpenStreetMap contributors" | Obligations (CC BY 4.0): attribute, link the licence, indicate that the data was modified ("bearbeitet": re-projected to EPSG:3035, tiled, segmented, classified, enriched), and do not imply BEV/ESA/EU endorsement. Stichtag: ALS epochs 20220915/20230915/20240915 are mosaic snapshot dates — real flight years per block in meta.acquisition. DOP series 20220128/20221027/20240625/20250415. OSM caveat: the `infrastructure` layer / JSON key contains OSM-derived (ODbL) data and is kept separate. Merging it into a redistributed database triggers ODbL share-alike for that database. Everything else is CC BY 4.0. No personal data: cadastre owner data is not in the open datasets nor in this API; EZ numbers are public cadastre attributes. ## Methods — Segmentation Pipeline Pipeline: fused gradient → Felzenszwalb over-segmentation → RAG merge → feature extraction → classification → grouping. ### Step 1: Fused Multi-Layer Gradient Sobel edge detection on each data layer, weighted sum: - CHM (nDSM=DSM−DTM): 0.25 — canopy/building edges - DTM: 0.20 — terrain breaks (roads, embankments) - DSM: 0.10 — surface breaks - NDVI: 0.20 — vegetation boundaries - NIR: 0.15 — material transitions - Green/Red/Blue: 0.05/0.03/0.02 — fine spectral detail Each layer smoothed (Gaussian σ=0.8), Sobel-filtered, normalised to [0,1]. ### Step 2: Two-Layer Felzenszwalb + RAG Ground (nDSM<0.3m) and elevated (≥0.3m) segmented separately to prevent tree/ground merging. - Felzenszwalb: scale=150 (elevated) / 180 (ground), sigma=0.5, min_size=30px / 60px - RAG merge: threshold=0.12 — adjacent segments with similar gradient are merged ### Step 3: Feature Extraction (57 features per segment) - Height & terrain (8): h_mean, h_max, h_std, h_p90, slope_mean, slope_std, dsm_roughness, dtm_roughness - Shape (5): compactness, elongation, solidity, extent, area - DSM edges (1): dsm_edge_strength - Spectral/BEV (11): ndvi_mean, ndvi_std, brightness_mean, nir_mean, red/green/blue_mean, green_ratio, rg_index, nir_brightness_ratio, nir_red_ratio - Copernicus (3): cop_ndvi_mean, fused_ndvi_mean, fused_ndvi_std - ESA WorldCover (5): esa_built_frac, esa_tree_frac, esa_crop_frac, esa_grass_frac, esa_water_frac - Temporal (8): h_change, dtm_change, dtm_change_abs, temporal_h_std, stability, volume_change_m3, volume_change_abs_m3, dtm_change_max, dtm_change_frac_03m - GLCM texture (6): glcm_contrast, glcm_homogeneity, glcm_entropy, glcm_dissimilarity, glcm_energy, texture_complexity - SAR (3): sar_vv, sar_vh, sar_ratio - NDVI harmonics (4): harm_mean, harm_amplitude, harm_phase, harm_rmse - Hansen (6): hansen_treecover2000, hansen_loss_frac, hansen_recent_loss_frac, hansen_loss_3yr_frac, hansen_gain_frac, hansen_current_forest_frac - Additional (5): ndvi_max, slope_max, h_p10, perimeter, esa_dominant_lc ## Methods — Classification Two-tier: Random Forest (primary) with rule-based decision tree (fallback). ### Random Forest Classifier - scikit-learn RandomForestClassifier: n_estimators=200, max_depth=20, min_samples_leaf=5, class_weight="balanced", oob_score=True - 16 learnable classes: tree, shrub, grass, crop, road, path, parking, roof, water, bare_soil, rock, earthwork, garden, orchard, vineyard, tree_loss - 3 excluded (rule-based only): wind_turbine, substation, solar_panel (need spatial context) - excavation + fill merged → earthwork for training; split back at inference by DTM change sign - If RF confidence < threshold → falls back to rule-based classifier ### Rule-Based Fallback (decision tree, ordered by priority) 0. Infrastructure spatial match: known solar/wind/substation locations from austria-power API → early exit 1. Temporal disturbance: DTM change >0.8m → excavation/fill; nDSM drop >2m → tree_loss/construction; nDSM grow >2m → construction. NDVI>0.5 vetoes disturbance. 2. Water: ESA water >50% + NDVI <0.1 + flat; or very low NIR + negative NDVI 3. Elevated (nDSM>0.5m): building score from DSM roughness + height variance + compactness + NDVI + texture + SAR. High score → roof/greenhouse/solar_panel. Low score → tree/shrub/hedge. 4. Ground (nDSM≤0.5m): NDVI harmonics primary (high amplitude→crop, low mean→road, moderate→grass). SAR fallback. Shape (elongated→road). ESA priors. Terrain (steep+rough→rock). NDVI selection hierarchy: fused NDVI (1m+seasonal) > BEV ortho NDVI (1m) > Copernicus 10m NDVI. Coarse 10m NDVI triggers extra caution (vegetation bleed into roads at boundaries). ### Hierarchical Grouping Adjacent compatible segments merge into 11 group types: - forest (tree+shrub+hedge), woodland (shrub+hedge), hedgerow (hedge) - waterbody (water) - building (roof+wall+solar_panel+greenhouse+substation) - road_network (road+path+parking) - cropland (crop+grass), pasture (grass+garden), orchard_grove (orchard+vineyard) - quarry (excavation+fill), construction_site (construction+excavation+fill+tree_loss) ## Methods — RF Training Pipeline ### Ground Truth Sources 1. Cadastre land-use codes (Benützungsart): 50-entry BA→type mapping (42→roof, 51→crop, 56→tree, 48→road, etc.) 2. OSM: roads/paths buffered to width → road/path labels; landuse/natural polygons → forest/farmland/water/quarry labels 3. Hansen tree_loss: synthetically labelled where hansen_recent_loss_frac≥0.15 + treecover2000≥20% + (height<5m or h_change<-2m) - Label circularity fix: hansen_recent_loss_frac and hansen_treecover2000 features zeroed for tree_loss samples ### Training Process Runs as systemd service (rf_train.service). Iterates ~300 random KGs (seed 42, >5 buildings each) + infrastructure KGs: 1. For each KG: fetch cadastre + OSM ground truth, read all raster data, segment, extract 57 features, match to labels 2. Each KG checkpointed to rf_training_data/checkpoints/kg_XXXXX.npz (skipped on restart) 3. Every 10 KGs: retrain RF on all accumulated samples, save to /tmp/learned_classifier/ 4. Dominant classes downsampled to 5× median class count to reduce imbalance ### Model Selection - evaluate_checkpoints.py trains RF at every 5-KG increment, multiple seeds - Composite score: 0.4×OOB + 0.35×mean_per_class_accuracy + 0.25×worst_class_accuracy - Best checkpoint+seed saved to data/best_model/ — API prefers this over live model - Training Monitor dashboard: /training.html (live OOB curve, per-class accuracy, convergence) ### Deployed Model (data/best_model/) Selected at peak composite score from learning curve: - Training: 55 KGs, seed 2, 87,496 samples, 16 classes - Hyperparams: n_estimators=200, max_depth=20, min_samples_leaf=5, class_weight="balanced" - OOB accuracy: 69.9%, composite: 59.0%, mean per-class: 65.0%, worst class: 33.0% (earthwork) Per-class OOB accuracy: Vegetation: tree 86%, crop 83%, garden 80%, shrub 63%, grass 59%, vineyard 54%, orchard 44% Water: water 52% Buildings: parking 77%, roof 68% Transport: road 72%, path 35% Terrain: bare_soil 78%, rock 56%, earthwork 33% Change: tree_loss 100% Top 10 feature importances: hansen_loss_frac 3.5%, hansen_treecover2000 3.5%, dtm_roughness 3.2%, fused_ndvi_mean 3.2%, slope_mean 3.2%, h_mean 2.7%, dtm_change_abs 2.6%, temporal_h_std 2.3%, h_p90 2.3%, h_p10 2.2% Learning curve: composite peaks at 55 KGs then declines (additional data introduces label noise from cadastre/OSM misalignment faster than new signal). OOB and composite both trend down after 60 KGs. ## Austria-Wide Processing (Austria Processor) Background processor (austria_processor.py, ~5100 lines) applies the full pipeline to all ~8,440 Austrian Katastralgemeinden (KGs), uploading results to Zenodo as open-access datasets. ### Architecture - Runs as systemd service (austria_processor.service, MemoryMax=4G) - Each KG processed in isolated subprocess (multiprocessing.Pool(1)) — full memory reclaim between KGs - Large KGs tiled into 1.5km overlapping strips (100m overlap), centroid-ownership deduplication - Tile checkpoints: completed tiles pickled to disk; on crash only interrupted tile re-processed - Grid-snapped caches: Copernicus (0.1°) and Hansen (0.5°) tiles shared by adjacent KGs - Zenodo cache: Copernicus/Hansen tiles persisted as ZIP archives, restored via HTTP range reads on local miss ### Per-KG Outputs (uploaded to Zenodo) - {kg}_full.gpkg (~400MB): all raster layers (DTM/DSM/nDSM/ortho/segment_type) + vector segment polygons - {kg}_light.gpkg (~100MB): segment raster+vector, parcels with DTM heights, buildings with object heights, new buildings, infrastructure - {kg}.json (~1MB): area statistics, height distributions, landscape characterisation, top objects, terrain, NDVI, Hansen loss, new buildings, infrastructure, data quality score #### GPKG segments layer — full per-object attribute schema (55 props) Present in {kg}_full.gpkg and {kg}_light.gpkg (layer 'segments', MultiPolygon EPSG:3035) and in app GPKG exports (onestop, /export/geopackage, share download). A companion 'segment_points' layer (Point EPSG:4326, 54 props) carries the same attributes on object centroids. Identity: id, type, type_code, group_type, height_class Shape: area_sqm, perimeter_m, compactness, elongation, solidity, extent Height: height_max_m, height_mean_m, height_p90_m, height_std_m Terrain: slope_mean_deg, aspect_mean_deg, aspect_dominant, roughness, dsm_edge_strength, elevation_mean_m, elevation_min_m, elevation_max_m, tri_mean, tpi_mean, curvature_mean, terrain_class Spectral: ndvi_mean, ndvi_std, ndvi_fused, brightness_mean, nir_mean Temporal: height_change_m, dtm_change_m, temporal_stability, volume_change_m3, volume_change_abs_m3, dtm_change_max_m Texture (GLCM): glcm_entropy, glcm_homogeneity, texture_complexity SAR: sar_vv, sar_vh Phenology: harm_amplitude, harm_phase, phenology_class Classification: confidence, is_manmade, classifier (rf|rules|infra), rf_model (model hash), rf_type, rf_confidence Rendering: color (by type), color_height (viridis height ramp) Observation: obs_year A QGIS layer_styles table auto-renders both layers (colour by type). #### GPKG parcels layer — per-parcel terrain & AMS anchor fields elevation_min_m float, min DTM elevation within parcel elevation_max_m float, max DTM elevation within parcel elevation_range_m float, elevation_max − elevation_min slope_mean_deg float, mean slope in degrees aspect_mean_deg float, mean aspect 0-360 (-1 = flat) aspect_dominant str, compass octant (N/NE/E/SE/S/SW/W/NW/flat) tri_mean float, Terrain Ruggedness Index (mean) tpi_mean float, Topographic Position Index (mean) terrain_class str, Riley classification (level … extremely_rugged) vertex_heights AMS anchor points: DTM elevation at every boundary vertex GPKG format: pipe-separated string "lat,lon,dtm_m|lat,lon,dtm_m|..." JSON format (in KG .json summary): array of {lat, lon, dtm_m} objects #### GPKG buildings layer — enhanced building footprints building_id str, cadastre building identifier ns str, Nutzungssymbol (cadastre usage symbol) full_address str, composed address house_number str street str postal_code str location str, locality/Ortschaft name max_height_m float, DSM max − DTM at footprint mean_height_m float, DSM mean − DTM at footprint dsm_std float, DSM standard deviation within footprint roof_type_hint str, "flat" or "pitched" (inferred from dsm_std) stories_est int, estimated storey count from max_height_m footprint_area_sqm float, polygon area in m² centroid_lon float, WGS84 longitude of centroid centroid_lat float, WGS84 latitude of centroid centroid_dtm_m float, DTM elevation at centroid vertex_heights AMS anchor points: DTM + object height at every footprint vertex GPKG format: pipe-separated "lat,lon,dtm_m,obj_height_m|lat,lon,dtm_m,obj_height_m|..." JSON format (in KG .json summary): array of {lat, lon, dtm_m, obj_height_m} objects ### Zenodo Integration Each KG gets its own Zenodo deposit with DOI. Search index stores download URLs: - zenodo_json_url, zenodo_light_gpkg_url, zenodo_full_gpkg_url - /api/v1/query and /api/v1/kg/ include these URLs when available ### Fault Tolerance - Tile checkpoints survive crashes/restarts - Automatic retry on timeout (90min), deferred retry 5 KGs later for transient failures - OOM: systemd kills + auto-restarts, checkpoints preserved - Disk management: LRU eviction, auto-pause below 3GB free - Copernicus circuit breaker: skip openEO on 503s for cooldown period - failed_kgs.json: permanently failed KGs skipped on restart ### Processor Dashboard & API - Dashboard: /process.html (live progress, map, tile status, log, Zenodo manifest) - GET /api/v1/processing/status — current state, KG, step, tile progress, rates - POST /api/v1/processing/start — start (optional: state=, kg=) - POST /api/v1/processing/pause|resume|stop — control - POST /api/v1/processing/single?kg=X — process single KG - GET /api/v1/processing/log — recent log lines - GET /api/v1/processing/manifest — Zenodo manifest entries - GET /api/v1/processing/queue — read priority queue (items, names, failure counts, permanently failed) - POST /api/v1/processing/queue — add KGs at position (body: {kgs, position, skip_processed}) - PUT /api/v1/processing/queue — replace entire queue (body: {queue: [...]}) - DELETE /api/v1/processing/queue?kg=X — remove KG from queue Queue management: The processor reads retry_queue.json before its normal nearest-neighbor traversal. POST inserts KGs at a 0-based position (default -1 = append). Already-processed KGs are skipped by default. Duplicates in the queue are moved to the new position. Response includes resolved KG names, gemeinde, and final positions. Examples: # Read the queue curl https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue # → {"queue": [{"code":"12105","name":"Dürnstein",...}, ...], "count": 28, "permanently_failed": [...]} # Add Gesäuse + Kalkalpen national park KGs starting at position 2 (3rd place) curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue \ -H 'Content-Type: application/json' \ -d '{"kgs":["49006","49013","49106","49311","49313","49321","49405","49406","49407","49409","49412","60102","60106","60107","67106","67107","67109","67111","67404","67405","67412","67501"],"position":2}' # → {"status":"added","added_count":22,"queue_length":50,"added":[{"code":"49006","name":"Innerbreitenau","position":2},...]} # Move a KG to the front of the queue curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue \ -H 'Content-Type: application/json' \ -d '{"kgs":["91109"],"position":0}' # Replace entire queue curl -X PUT https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue \ -H 'Content-Type: application/json' \ -d '{"queue":["49006","49013","60102"]}' # Remove a KG from the queue curl -X DELETE 'https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue?kg=49006' ## Geometry Input (all POST endpoints) Body accepts: GeoJSON, `{"geometry": , ...params}`, KML string, coordinate string (`lon,lat` or `lon,lat;lon,lat;...`), or multipart file upload (KML, GeoJSON, Shapefile ZIP, GPX, WKT, GeoPackage). Multi-feature inputs are unioned. Points are buffered to 100m radius. --- ## Endpoints ### Analysis #### POST /api/v1/segment ← PRIMARY Watershed segmentation → 25 object types + 11 group types. Query/body params: dataset ALS date (default: 20240915) min_object_size Min segment area in m² (default: 30) felz_scale Felzenszwalb scale (default: 150) rag_threshold RAG merge threshold (default: 0.12) include_ortho true → BEV RGBI (NDVI, NIR, brightness, GLCM texture) include_temporal true → 3-date DTM comparison (2022/2023/2024) include_copernicus true → Sentinel-2 NDVI + ESA WorldCover + SAR + NDVI harmonics include_cadastre true → cadastre building footprint ground truth include_hansen true → Hansen Global Forest Change calibration types Comma-separated type filter (e.g. "roof,tree,road") groups Comma-separated group filter (e.g. "building,forest") async true → returns 202 with {task_id}, poll for progress task_id Client-provided UUID for tracking Async flow: 1. POST /api/v1/segment?async=true → 202 {task_id, status: "running"} 2. GET /api/v1/segment/progress?task_id=X → {active, step, detail, elapsed, done, error, auto_share_id} 3. GET /api/v1/segment/result?task_id=X → full result 4. POST /api/v1/segment/abort?task_id=X → cancel Result: FeatureCollection. Per-feature properties: type, type_code, group_id, group_type, is_manmade, confidence, classifier_source ("rf"|"rules"|"infra"), rf_type (original RF prediction), rf_confidence, height_max_m, height_mean_m, height_p90_m, area_sqm, compactness, elongation, solidity, extent, dsm_edge_strength, slope_mean, roughness, ndvi_mean, ndvi_fused, nir_mean, brightness_mean, height_change, dtm_change, temporal_stability, glcm_entropy, glcm_homogeneity, texture_complexity, sar_vv, sar_vh, harm_amplitude, harm_phase, phenology_class Terrain enrichment (per-segment, also in GPKG segments layer): aspect_mean_deg float, 0-360 degrees (-1 = flat terrain) aspect_dominant str, compass octant: N/NE/E/SE/S/SW/W/NW/flat elevation_mean_m float, mean DTM elevation in metres elevation_min_m float, minimum DTM elevation elevation_max_m float, maximum DTM elevation tri_mean float, Terrain Ruggedness Index (mean within segment) tpi_mean float, Topographic Position Index (mean; +ve=ridge, -ve=valley) curvature_mean float, profile curvature (mean; +ve=convex, -ve=concave) terrain_class str, Riley et al. classification: level / nearly_level / slightly_rugged / intermediately_rugged / moderately_rugged / highly_rugged / extremely_rugged Completed async tasks are auto-saved as shares (auto-). #### POST /api/v1/elevation Enrich features with DSM/DTM elevation. #### POST /api/v1/terrain Terrain characterisation: slope, aspect, TRI, TPI, curvature. Invalid input polygons (self-intersections, ring sets where a hole is emitted as the exterior — common with raw cadastre geometry) are repaired server-side with make_valid() before rasterising. When that happens the response carries meta.geometry_repaired=true and meta.warnings[]. Applies to all geometry-driven endpoints (/terrain, /elevation, /segment, /changes*). #### POST /api/v1/changes Temporal change detection between two ALS dates. Params: date_a (default: 20220915), date_b (default: 20240915), min_change (m, default: 1.0) 20 event types: tree_growth, tree_felling, new_tree, forest_clearcut, vegetation_growth/loss, new_building, demolition, construction, earthwork_fill/cut/grading/dam/trench/pond, road_new/resurfaced/widened, surface_change, unclassified_change. #### POST /api/v1/changes/trees Per-tree growth/felling analysis between dates (matches trees across epochs by centroid proximity; statuses: grown, stable, felled, new). Params: date_a, date_b ALS dates (default: 20220915, 20240915) min_tree_height Min nDSM peak to count as a tree in metres (default: 3.0) crown_geometry point (default) | polygon — return crown outlines (after-date crown preferred, before-date for felled trees) async true → 202 {task_id, poll, result}; poll /api/v1/segment/progress, fetch /api/v1/segment/result. Recommended for areas > ~50 ha. Per-tree properties: tree_id, status, height_before_m, height_after_m, height_change_m, crown_area_before_sqm, crown_area_after_sqm. Summary: total_trees, by_status{count, height_change_mean_m}. v2.3: NON-FOREST SUPPRESSION also runs here (params reject_nonforest / include_cadastre / min_tree_likelihood, defaults as in /api/v1/trees). Load-bearing for change detection: without it a demolished barn is reported as 'felled' timber and a new shed as a 'new' tree. Objects are judged on the epoch where they exist (after-date crown if present, else before-date). Measured on a farmyard AOI: 95 → 51 changes, 44 rejected (29 building, 13 crop, 2 flat_structure). meta carries rejected_by_surface / rejected_by_surface_class / building_mask. #### POST /api/v1/trees Single-date per-tree inventory (crown detection on nDSM watershed + crown-shape analysis, optional orthophoto spectral support). Forestry-oriented. Params: dataset ALS date (default: 20240915) min_tree_height Min nDSM peak in metres (default: 3.0) crown_min_area Min crown footprint in pixels/m² (default: 4) crown_geometry point (default) | polygon — crown outline polygons (WGS84) include_ortho true (default) | false — use BEV ortho for NDVI/leaf type reject_nonforest true (default) | false — v2.3 NON-FOREST SUPPRESSION, the SAME engine as /api/v2/trees (see there for the full evidence chain). v1's own building pixel class is geometry+spectral only and leaks badly: on a Waldviertel farmyard AOI it reported 175 trees, 85 of them standing on cadastre building footprints. With suppression on: 69 trees, 2 on footprints (both genuine crowns overhanging a roof). include_cadastre true (default) | false — fetch BEV building footprints as authoritative non-tree evidence min_tree_likelihood 0..1 (default 0.35) — reporting threshold async true → 202 {task_id, poll, result} (same task framework as /segment). Recommended for areas > ~50 ha. Per-tree properties: tree_id, height_max_m, height_mean_m, height_p90_m, crown_area_sqm, crown_shape (conical|rounded|columnar|irregular), leaf_type (coniferous|broadleaf|dead|unknown — heuristic from crown shape + spectra; treat as a hint, not species truth), compactness, elongation, ndvi_mean (when ortho available), surface_class + tree_likelihood (v2.3). Summary: total_trees, stems_per_ha, by_leaf_type, height_mean_m, height_max_m, top_height_m (h_dom — mean of the 100 tallest stems per ha), crown_area_total_sqm, crown_cover_pct. meta also carries rejected_by_surface / rejected_by_surface_class / building_mask / nonforest_rejection. NOTE: for forestry-grade stem density / height distributions prefer the /api/v2 tree service below — v1 watershed under-segments closed conifer canopy ~2x and its summary uses the whole-AOI denominator. Non-forest suppression quality is now IDENTICAL in both (shared engine), so the choice is purely about detection: v2 for inventory work, v1 only if you need its crown_shape / segment-based object semantics. ### v2 tree service (apex-based, forestry-grade) #### POST /api/v2/trees Apex-based single-tree inventory, v2.3 (ortho-fused detection + NON-FOREST SUPPRESSION). NON-FOREST SUPPRESSION (v2.3, on by default). An nDSM local maximum is only reported as a stem when the surface under it behaves like a crown. Roof ridges, silos, containers, hay stacks, maize/rape plateaus, rock and road furniture all raise the DSM above terrain and used to be reported as trees with full DBH/volume (bogus crowns over farmyards and fields). Every crown is now judged on evidence, in order of authority: 1. CADASTRE — crown apex on, or >=50% of crown inside, a BEV building footprint (dilated 1 m: outlines are wall lines, the DSM sees eaves) => surface_class='building'. 2. NDVI (0.2 m BEV RGBI) — mean crown NDVI <= 0.10 means no live foliage => 'hard_surface'. The one legitimate NDVI-poor tree, a dead standing stem (small AND geometrically rough crown), is kept as 'dead_tree_candidate' rather than silently dropped. 3. SURFACE GEOMETRY — plane-fit RMS residual, median |Laplacian| roughness and apex-minus-crown-p10 relief, all measured on the crown CORE (crown eroded 1 px, so the height cliff at the object edge does not fake roughness). Planar + smooth + flat-topped => 'crop' (<=6 m, i.e. field crops) or 'flat_structure' (taller); borderline cases => 'uncertain'. Strong vegetation evidence (crown NDVI >= 0.30) overrides the geometry-only verdicts, so a broad flat-crowned solitary oak on a lawn stays a tree — but it does NOT override 'crop', because maize is the greenest thing in the scene; the 6 m height ceiling is what protects real trees there. Rejected crowns are removed from the label raster AND from the canopy mask, so stems_per_ha / crown_cover_pct denominators no longer count a farmyard or a maize field as stocked area. Every reported tree carries surface_class + tree_likelihood (0..1) + surface_building_frac / surface_roughness_m / surface_planarity_m / surface_relief_m, so a client can re-threshold or audit the verdict. Set reject_nonforest=false to receive ALL apices, classified but unfiltered (useful to see what was removed and why). Measured on a Waldviertel farmyard AOI (0.008° x 0.005°, 47 cadastre buildings, 5.8% of grid): 650 → 206 stems, apices on building footprints 317 → 4. On a forest-dominated hamlet AOI the filter is nearly a no-op (4829 → 4734, 95 rejected), i.e. it costs almost no recall where there is nothing to reject. Cost is ~0.1 s + one cached cadastre call. Seeding is two-source: (1) variable-window local maxima on nDSM (smoothed sigma=smooth_sigma), window radius r(h) = crown_radius_a + crown_radius_b*h; (2) native-resolution (0.2–0.5 m) RGBI ortho apex candidates — DoG band-pass crown-cap maxima (NIR preferred, RGB luminance fallback), height-gated on the nDSM (no stems below canopy) and restricted to locally CLOSED canopy (>=70% closure in 15 m) where the 1 m first-return gridding merges neighbouring tops. Both seed sets fuse into one marker-controlled watershed; crown radius capped at r(h)*crown_cap_factor and max_crown_area. Params: dataset ALS date (default: 20240915) min_tree_height m (default 3.0) reject_nonforest true (default) | false — drop buildings/crops/hard surfaces (see NON-FOREST SUPPRESSION above) include_cadastre true (default) | false — fetch BEV building footprints as authoritative non-tree evidence. false skips the cadastre round-trip (faster, and the only option outside cadastre coverage); the NDVI + geometry gates still run. meta.building_mask reports what was actually used. min_tree_likelihood 0..1 (default 0.35) — reporting threshold on the surface score. Raise to be stricter (0.6 keeps only crowns with no non-tree evidence at all), lower to recover borderline field trees. crown_radius_a m, local-max window base (default 1.2) crown_radius_b m per m height (default 0.08) smooth_sigma Gaussian sigma in px/m (default 0.5) crown_cap_factor crown radius cap multiplier (default 1.5) max_crown_area m² hard cap per crown (default 250) crown_geometry polygon (default) | point include_ortho true (default) — 0.2 m RGBI spectra when covered detection_mode fused (default) | ndsm_only — fused adds the native-res ortho seeding; falls back to ndsm_only automatically when no ortho covers the AOI (see meta.detection_mode + detection_fallback_reason) ortho_seed_res_m target seeding resolution (default 0.4; snapped to an integer divisor of 1 m, coarsened for huge AOIs) min_apex_prominence_m m (default 0 = off) — filter apices by h-maxima prominence; suppresses branch-level false apices leaf_type_min_conf 0..1 (default 0.5) — below this leaf_type='unknown' h_dom_basis canopy (default) | total — area basis for h_dom async recommended for areas > ~50 ha Per-tree properties: tree_id (STABLE, location-derived: t__, EPSG:3035 apex rounded to 1 dm — deterministic across re-runs and epochs), seq, apex_lon/apex_lat (the local maximum, for field navigation), height_m (raw nDSM at apex), crown_area_sqm, crown_radius_mean_m/max_m, is_edge (crown touches AOI boundary — exclude from density), dbh_est_cm + dbh_method (heuristic), volume_m3_est, leaf_type + leaf_type_conf + leaf_type_prob_conifer (raw probability — re-threshold client-side), species_hint (spruce|larch|pine|conifer_unspecified| broadleaf_unspecified|unknown) + species_conf (capped 0.6 — ASSUMPTION-grade: relative within-AOI RGBI spectral position of the conifer population; spruce=dark crowns, larch=bright fresh-green deciduous conifer, pine=intermediate; needs >=10 conifers in AOI and leaf-on imagery — keep out of hard economic gates), vitality (vital|stressed|dead|unknown) + vitality_conf, detection_source (ndsm | ortho | fused) + detection_conf (0..1): 'ndsm' = LiDAR local maximum (conf 0.75), 'fused' = nDSM apex independently confirmed by an ortho crown cap (conf 0.9), 'ortho' = recovered ONLY by native-res ortho seeding (conf 0.30–0.75 by seed strength — render differently, keep out of hard economic gates). Vitality is RELATIVE: 'stressed' = NDVI <= p10 within the AOI's same-leaf-type population (per-tree spectral.ndvi_percentile_in_aoi lets you re-cut); 'dead' stays absolute (ndvi_mean<0.15 & ndvi_p10<0.10 & h>=5). Only trustworthy when meta.nir_used=true. Also ndvi_mean/ndvi_p10/nir_mean/brightness_mean (when spectra available). Summary (explicit denominators): n_trees, n_trees_edge, area_ha_total, area_ha_canopy (nDSM >= min_tree_height), stems_per_ha_canopy, stems_per_ha_total (both non-edge only), h_mean_m, h_p50/p90/p95/p99/max_m, h_dom_m (mean of 100 tallest per ha over CANOPY area by default — see h_dom_basis/h_dom_note), h_top100_m (tallest stem per 100 m grid cell, robust moving-hectare Oberhöhe), crown_area_total_sqm, crown_cover_pct_canopy, crown_cover_pct_total, height_histogram_2m (2 m bins, STEMS — detection-biased below ~10 m under closed canopy), layer_profile_2m (nDSM PIXEL heights in 2 m bins — use for structure/ plenter tests), canopy_gap_fraction (share of AOI pixels 2–10 m — where low stems are actually visible to first-return DSM), understory_detectable (always false: suppressed stems below dominant layer are invisible to a 1 m first-return DSM — see detection_floor_note), by_leaf_type, by_vitality, by_species_hint, by_detection_source, by_surface_class (+ surface_class_note) — surface verdict of the REPORTED stems; with reject_nonforest=false the non-tree classes (building / hard_surface / crop / flat_structure / uncertain) show how many apices are not trees, recall_model {canopy_area_ha, crown_area_ha, unassigned_canopy_frac — self-reported residual under-detection: the share of canopy >= min height that belongs to no detected crown}, volume_m3_est_total, volume_method (full allometry formula + known bias; generic spruce heuristic, NOT Pollanschütz — calibration against plot data available on request). meta: tree_algo_version, params_hash (deterministic over effective params — same params → same result; invalidate caches when tree_algo_version changes), detection_mode (fused | ndsm_only — what actually ran) + detection_fallback_reason when degraded, seed_band (nir | luminance), n_seeds_ndsm / n_seeds_ortho_candidates / n_seeds_ortho_added / n_seeds_ortho_pruned (added seeds whose final crown < 4 m² are treated as intra-crown branch texture, dropped, and the watershed re-run — no confetti crowns), detection_mode_requested (the param echo; meta. detection_mode is what actually ran), rejected_by_surface (count of apices dropped as non-forest) + rejected_by_surface_class ({building: N, crop: N, hard_surface: N, …}) + nonforest_rejection (bool, whether suppression was active) + building_mask (cadastre | unavailable | skipped… | error: …) + building_mask_px (footprint pixels on the AOI grid), nir_used_for (list: classification / detection / detection_luminance — replaces the ambiguous nir_used flag, which is kept for compatibility), acquisition (REAL flight years per BEV flight-block overlay: blocks[{flugjahr, year_from/to, gebiet, coverage_frac}], effective_date — the folder date 20220915 etc. is a mosaic snapshot, NOT the flight date; e.g. Steiermark '20220915' was flown 2010), ortho_operates/ortho_epoch (which RGBI operate fed the spectra), all effective parameters echoed. #### POST /api/v2/changes/trees Apex-matched change detection that survives re-segmentation. Matching pass 1: greedy nearest-neighbour on APEX points, radius max(match_radius_m, 0.75*r(h)). Pass 2: apex-unmatched trees whose crown masks overlap >= 30% of the smaller crown across epochs are the same tree (apex jumped to another branch) — match_method=apex|crown|none, crown matches carry crown_overlap_frac. Raster veto: 'felled' requires max nDSM in 3 m radius at date_b to have dropped >= felling_min_drop_m AND be < 40% of date_a height. Veto failures are SUB-CLASSIFIED, not lumped: unmatched_a_canopy_intact date_b canopy >= 70% of date_a height — matching failure, NOT a loss unmatched_a_partial_drop 30–70% height remains — crown break / snow / wind damage unmatched_a_ambiguous the rest 'new' requires date_a nDSM < max(3 m, 30% of date_b height), else unmatched_b_canopy_preexisting (apex churn, not a new tree). Statuses: stable | grown | shrunk | felled | new | unmatched_a_* | unmatched_b_canopy_preexisting. unmatched_a_canopy_intact and unmatched_b_canopy_preexisting records also carry match_status='unmatched_recall' — the counterpart epoch had no detection but the canopy is intact, i.e. a detector-recall artefact, NOT a physical change. Filter these out of loss/gain accounting. Detection here is FORCED symmetric ndsm_only for BOTH epochs (fused seeding disabled), so a recall difference between detector generations can never masquerade as new/felled records; ortho still feeds epoch-b vitality/leaf-type when include_ortho=true. Params: date_a, date_b, match_radius_m (default 3.0), felling_min_drop_m (default 5.0), growth_eps_m (default 0.3), min_patch_sqm (default 25), crown_geometry=point (default)|polygon, + all /api/v2/trees params. Per-record: tree_id (epoch-a id; tree_id_b when matched), status, match_method, height_a_m, height_b_m, height_change_m (null when not matched — never reused as a height), growth_cm_yr, volume_m3_est (felled records: allometric volume of the lost tree), crown_area_a/b_sqm, ndsm_max_a/b_m (the raster evidence at the apex, for audit without re-downloading rasters), match_distance_m, match_confidence, apex_a_lon/lat, apex_b_lon/lat. growth_cm_yr normalisation (IMPORTANT): the ALS folder dates are mosaic snapshots, not flight dates. When the BEV flight-block overlay knows the real Flugjahr for the AOI, growth is normalised by days_effective (mid-year assumption) — e.g. Steiermark 20220915→20240915 is really 2010→2022 = 4383 days, not 731. summary.growth_cm_yr_is_nominal=true flags AOIs where real dates are unknown. If both epochs were flown the same year (same_flight_epoch=true) the rasters are likely the same acquisition republished — treat height changes as noise. Response also carries: felling_patches FeatureCollection of contiguous raw-nDSM-drop polygons (>= min_patch_sqm, despeckled) with area_sqm, drop_mean_m, drop_max_m, height_before_mean_m — the segmentation-independent disturbance product. epoch_dates {a, b, days, days_nominal, days_effective, same_flight_epoch} summary by_status, n_trees_a/b, felled_area_sqm, felled_volume_m3_est (SUM of per-tree allometric volumes over status=felled — same allometry as /v2/trees), felled_volume_m3_patch (independent patch-based proxy: area_ha × height_before × 16 m³/ha per metre; formula in the *_note fields), growth_cm_yr_percentiles (p10/p25/p50/p75/p90). meta.acquisition {a, b} real flight-year blocks per epoch. #### POST /api/v2/trees/by-polygons Per-stand batch inventory. Body: GeoJSON FeatureCollection of stand polygons (WGS84; reproject client-side from EPSG:32633 etc.). Computed ONCE on the union raster; trees assigned by apex containment (no double counting at stand boundaries — but if the stand polygons themselves overlap, trees in the overlap are counted in every containing stand; see n_overlapping_pairs / overlap_area_ha in the response summary). Params: key_property (stand id property name, default 'name'), merge_by_key=true to union same-key features into one multipart stand (entry carries n_parts), include_trees=true to embed per-tree Features per stand, + all /api/v2/trees params. async strongly recommended. Null/missing keys → key: null + a warning (never silently renamed); duplicate keys without merge_by_key are flagged key_is_duplicate. Response: {stands: [{key, feature_index, summary, n_parts?, key_is_duplicate?, trees?}], summary: {n_stands, n_trees_total, n_trees_assigned, union_area_ha, sum_area_ha, n_overlapping_pairs, overlap_area_ha}, warnings, meta}. #### POST /api/v2/changes/trees/by-polygons Per-stand growth + mortality in ONE call. Body: FeatureCollection of stand polygons. Runs the full two-epoch change analysis once on the union raster, then buckets records into stands by apex containment (apex_b preferred, apex_a fallback for felled). Params: all /api/v2/changes/trees params + key_property, merge_by_key, include_records=true to embed the per-tree change records per stand. async strongly recommended. Response: {stands: [{key, feature_index, summary: {n_records, by_status, area_ha_total, growth_cm_yr_percentiles (p10/p50/p90), felled_volume_m3_est, mortality_pct}}], summary (whole-union, incl. overlap diagnostics), warnings, epoch_dates, felling_patches (union-wide), meta}. #### POST /api/v1/changes/summary Multi-epoch change summary (2022→2023→2024). ### Overlays (POST, return PNG with X-Bounds header: south,west,north,east) /api/v1/segment/overlay Segment classification raster. Params: types, color_mode (type|height) /api/v1/dtm/overlay DTM hillshade. Params: dataset /api/v1/lidar/overlay nDSM height (viridis). Params: dataset /api/v1/ortho/overlay RGB orthophoto. Params: dataset, ortho_year /api/v1/cir/overlay CIR false-colour (NIR→R, R→G, G→B). Params: dataset, ortho_year /api/v1/hansen/overlay Hansen forest change. Params: dataset ### Exports #### POST /api/v1/export/geopackage All layers in one GPKG. Supports async (async=true → poll → download). Params: layers all (default) | comma-separated layer IDs: dtm → DTM + DSM + nDSM (raw 1m float32) segments → Segment type + height rasters ortho-YYYY → Orthophoto RGBI for that year cir-YYYY → CIR false-colour for that year raster → Coloured segment overlay (RGBA) hansen → Hansen forest change overlay (RGBA) dtm-YYYY → DTM hillshade overlay (RGBA) dsm-YYYY → nDSM height overlay (RGBA) types Segment type filter (e.g. tree,road) height_min Height filter: keep segments >= X metres height_max Height filter: keep segments <= X metres height_op gt | lt | between (inferred from min/max if omitted) color_mode type | height (segment colouring) include_segments_vector true → add vector 'segments' (MultiPolygon, EPSG:3035) + 'segment_points' (Point, EPSG:4326) layers with the full 55-attribute schema (see "GPKG segments layer" above) async true → 202 with {task_id} Async download: GET /api/v1/export/geopackage/download/ Share shortcut: GET /api/v1/share//download.gpkg layers=all (default) | active (from share's UI state) | comma-separated IDs types, height_min, height_max, height_op → filter segments #### POST /api/v1/export/kml Export segment features as KML with type/height_class folders. Params: types Segment type filter (e.g. tree,grass) group_by type (default) | height_class — folder grouping segment_geometry point (default) | polygon — vectorise segments to polygons segment_geometry_style type (default) | height — colour by object type or viridis height ramp height_min Height filter: keep segments >= X metres height_max Height filter: keep segments <= X metres height_op gt | lt | between (inferred from min/max if omitted) #### POST /api/v1/export/mbtiles Single raster layer as MBTiles for offline use. Params: layer (required, e.g. dtm-2024, ortho-2024, raster), min_zoom, max_zoom, async Async download: GET /api/v1/export/mbtiles/download/ ### Raw GeoTIFF Downloads (POST) /api/v1/lidar/geotiff DTM/DSM/nDSM float32 GeoTIFF. Params: dataset, band=dtm|dsm|ndsm|all (default all → 3-band stack b1=DTM b2=DSM b3=nDSM). Single-band requests return a 1-band file. NoData=-9999 set; band descriptions set. /api/v1/ortho/geotiff Orthophoto GeoTIFF, RGB + NIR (band 4) when an RGBI operate covers the AOI/year. Params: dataset, ortho_year (e.g. 2024), resolution (m/px, default 1.0, min 0.2 = native; capped at 100 Mpx), or res=native|1m alias (native → 0.2 m). GeoTIFF tags: ORTHO_RES_M, ORTHO_EPOCH, ORTHO_OPERATE_ID. ### BEV source-data access (for local validation / offline work) ALS DTM/DSM (1 m, EPSG:3035, 50 km tiles, BigTIFF ~12 GB, HTTP range-requests supported — use GDAL /vsicurl/, no directory listing): https://data.bev.gv.at/download/ALS/{DTM|DSM}/{epoch}/ALS_{DTM|DSM}_CRS3035RES50000mN{N}E{E}.tif epochs: 20220915, 20230915, 20240915. Tile origin = lower-left corner in EPSG:3035 metres, aligned to the 50 000 m grid, no zero-padding beyond the natural 7 digits (e.g. N2650000E4700000). 55 tiles cover Austria; the authoritative tile list is tile_index.py:TILE_COORDS in https://github.com/ (this repo) — or derive: floor(coord/50000)*50000. Example (covers the Kohlschwarz/WILHELM AOI at E4708478 N2682287): https://data.bev.gv.at/download/ALS/DSM/20240915/ALS_DSM_CRS3035RES50000mN2650000E4700000.tif nDSM is NOT published — compute DSM - DTM (clamp negatives to 0). ALS flight dates (IMPORTANT): the epoch folder names (20220915 etc.) are national mosaic snapshot dates, NOT acquisition dates. Real per-block flight years come from the BEV "Aktualitaet DGM - ALS" overlay (Metadatenuebersicht shapefiles on www.bev.gv.at, ALS-Hoehenraster page; vendored in this repo at data/als_acquisition/*.geojson, queried by als_acquisition.py and surfaced as meta.acquisition on the v2 tree endpoints). Blocks can be over a decade apart between epochs — e.g. the Steiermark block in the '20220915' mosaic was flown 2010, while '20230915'/'20240915' carry a 2022 flight. Always use epoch_dates.days_effective (not the nominal folder-date span) for growth rates. Only Flugjahr (year, sometimes a range) is published — no month/day. Raw LiDAR point clouds (LAZ): BEV publishes ONLY the derived 1 m DTM/DSM rasters nationally — no national point-cloud download. Point clouds are held by the federal states (Laender): several offer open LAZ downloads (e.g. data.gv.at portals for Ober-/Niederoesterreich, Tirol, Kaernten, Stmk GIS), others on request. This service is raster-only. DOP orthophoto RGB (0.2 m, EPSG:3035, same 50 km grid): https://data.bev.gv.at/download/DOP/20220128/DOP_CRS3035RES50000mN{N}E{E}_20220128.tif RGBI operates (0.2 m RGB + separate NIR file, native Austrian GK CRS 31254/31255/31256, per-operate footprints): series folders https://data.bev.gv.at/download/DOP/{20221027|20240625|20250415}/ Operate ids, bboxes and CRS per operate: see ortho_io.py:RGBI_OPERATES. No public directory listing or STAC; range requests work; no special headers required. Per-AOI epoch availability oracle: GET /api/v1/layers?bbox=w,s,e,n (authoritative; /api/v1/info now lists the same epochs statically). ### Shares #### GET /api/v1/shares?limit=20 List saved shares, most recent first. Response: [{id, name, description, has_result, has_geometry, endpoint, updated, size_kb}, ...] #### POST /api/v1/share Save analysis state + result for sharing. Content-hash dedup. Body: {state, result?, overlays?, name?, reuse_id?} Response: {id, url, reused} #### GET /api/v1/share/ Load a share. Follows rename redirects (old links keep working). Response header X-Share-Id contains the resolved ID. Response: {state, result, overlays?, name?, aliases?} #### POST /api/v1/share//rename Rename a share. Old ID becomes a redirect stub so old links still work. Body: {new_id: "MySlug"} Response: {id, old_id} #### GET /api/v1/share//download.gpkg Direct GeoPackage download from a share (usable as QGIS data source URL). Params: layers (all|active|comma-separated IDs), types, color_mode ### Utilities #### GET /api/v1/layers?bbox=lon_min,lat_min,lon_max,lat_max Data layer availability for a bounding box. Response: {ortho: {"2024": bool, ...}, cir: {...}, dtm: {...}, dsm: {...}, hansen: bool} #### GET /api/v1/info Server info: data sources, datasets, object types, endpoints, licence + attribution block. #### GET /api/v1/attribution Licence + attribution for all upstream sources (see "Licence & Attribution" above). Params: format=text → plain-text block. Default JSON: {license, license_url, modification_note, osm_derived_layers, sources{id:{name,provider,license, license_url,attribution,citation?,share_alike?,url}}, short, text}. #### POST /api/v1/parse-geometry Parse uploaded geometry file (Shapefile ZIP, GeoPackage, GeoJSON, KML, GPX, WKT). Multipart upload with field "file". Returns GeoJSON FeatureCollection. #### POST /api/v1/classifier/train Train RF classifier on cadastre ground truth for a bbox. Params: bbox (lon_min,lat_min,lon_max,lat_max), n_estimators, max_depth #### GET /api/v1/classifier/status RF model status: trained, n_samples, oob_score, feature_importances. #### GET /api/v1/training/status Background RF training job progress (4000-KG run). ### Quality Flags & Feedback Every KG JSON is scanned post-build. Rules in `quality_flags.py` flag implausible heights, mismatched NDVI, edge artefacts, low-confidence classifications, and tree-vs-mast/hedge-vs-tree mistakes. Flags carry a `severity` (low|medium|high|critical) and a numeric `weight` so multiple rules firing on the same object accumulate ("agreement"). Empirical thresholds (re-tuned Apr-2026 against ~36k indexed parcels with top_objs/top_trees data): tree height warn 50m high 60m critical 80m (real Austria max ~57m) shrub height warn 8m high 12m critical 20m (was 4/6/15 — too noisy) hedge height warn 8m high 15m critical 25m + new rule: hedge ≥10m AND rf_conf 0.45–0.6 → likely_tree_row (medium) #### GET /api/v1/flags List flags. Filterable + paginated. Params: kg, severity, code, type, kind, obj_ref, bbox, min_value, order=severity|value|recent, limit, offset. Returns: {count, flags: [{obj_ref, kg_code, kind, obj_type, flag_code, severity, weight, message, attrs, aggregate}, ...]}. #### GET /api/v1/flags/object/ All flags + aggregates for one object. obj_ref examples: KGCODE:top_tree:N top_10_trees rank N KGCODE:top_obj:N top_10_objects rank N KGCODE:top_by_type:tree:0 per-type top entry KGCODE:building: cadastre-matched building KGCODE:new_building:N vectorised unmatched roof KGCODE:infra:road:N vectorised infrastructure feature KGCODE:parcel:PID parcel KGCODE:parcel_top_obj:PID:i per-parcel top object KGCODE:parcel_top_tree:PID:i per-parcel top tree #### GET|POST /api/v1/flags/match Match free text + optional coords to a known object so users can flag without knowing the obj_ref. Used by static/flag.js. Params: text="102.2m tree", kg=, lon=, lat=, radius_m=200. Returns: {obj_ref, candidates: [{obj_ref, distance_m, score, agg, ...}], flags: [...], action_predictions: {confirm, reject, correct_type}}. #### GET /api/v1/flags/stats Totals by code/severity/type. Useful for QA dashboards. #### POST /api/v1/feedback User confirms/rejects/corrects an object. Body: {obj_ref, kind: confirm|reject|correct_type|report_missing, value|corrected_type: ?, notes|note?, user?}. Both spellings are accepted (`notes`/`note`, `value`/`corrected_type`). Resolves matching flags atomically. Returns {ok, id, resolved_obj_ref, resolution_status, notes_stored, warnings?}. `warnings` lists silently-ignored fields (unknown keys, correct_type without a value, reject/report_missing with no note) — check it rather than trusting a bare ok:true. `user_role` is server-assigned ('student'); it is not settable by clients. #### GET /api/v1/feedback/events Recent feedback actions (live activity feed). ### Search Index (fast pre-built SQLite FTS5 + R-tree over all 8440 Austrian KGs) #### GET /api/v1/index/status Index statistics: kg_count, processed_count, total_area_km2, zenodo_kgs, etc. #### POST /api/v1/index/rebuild Rebuild search index from scratch (~0.3s). #### GET /api/v1/query ← UNIFIED QUERY ENDPOINT Supports multiple query modes via query params. All return JSON. Pagination: All list-returning query modes return a paginated envelope: {"total": , "offset": , "limit": , "results": [...]} Use limit= and offset= params to paginate. Default limit=100, max 1000. "total" is the full count of matching records (before limit/offset). Single-item lookups (kg=, parcel=) return the item directly (no envelope). Aggregate queries (aggregate=true) return the aggregate object directly. Async support: Slow queries (parcels_by_type, top_features) support async=true. 1. GET /api/v1/query?parcels_by_type=tree&min_confidence=0.8&async=true → 202 {task_id, status: "running", poll: "/api/v1/query?task_id=..."} 2. GET /api/v1/query?task_id= → 202 {active, task_id, step, detail, elapsed} while running → 200 {total, offset, limit, results} when done → 500 {error} on failure Also available: GET /api/v1/query/progress?task_id= Async results auto-cleaned after 4 hours. Speed tiers: FAST (<25ms) Index queries: bbox, admin, type ranking, confidence, divergence, compound, segments MEDIUM (~1s) JSON scan (pre-filtered): parcels_by_type, top_features, parcel lookup SLOW (~10s) GPKG download+query: /api/v1/kg//buildings, segments, etc. Confidence types: rf_confidence Raw Random Forest model prediction score (0-1). The direct output of the trained RF classifier. Available on all RF-classified segments. confidence Final/combined score after calibration and overrides (0-1). May differ from rf_confidence when: (a) a segment's type was changed by calibration (e.g. RF said "road" but calibration changed to "grass"), (b) confidence was penalised for anchor-weak or edge-clipped segments, or (c) rules-based classification was used as fallback. For undiverted RF segments, rf_confidence == confidence. For diverged segments, confidence is typically lower. Use min_rf_confidence= for strict ML-based filtering. Use min_confidence= for overall quality. Common questions → endpoints: "Parcels where tree RF conf > 0.8, area > 1500m²" → parcels_by_type=tree&min_confidence=0.8&min_area_sqm=1500 "Tallest trees with RF conf > 0.9" → top_features=trees&min_confidence=0.9 "New buildings in area X with RF roof conf > 0.75" → top_features=new_buildings&min_confidence=0.75&bbox=... "Infrastructure masts with RF conf > 0.8" → top_features=infrastructure&type=mast&min_confidence=0.8 "Multiple types + terrain + no buildings" → /api/v1/query/compound?type_filter=tree:0.8:800&max_buildings=0&aspect=S,SW,W "Top 1% tallest trees with RF conf > 90%" → segments=true&object_type=tree&min_rf_confidence=0.9&percentile=0.01 "Top 5% largest tree loss areas, RF conf > 0.8" → segments=true&object_type=tree_loss&min_rf_confidence=0.8&percentile=0.05&sort=area_sqm "Top excavations by volume, RF conf > 0.7" → segments=true&object_type=excavation&min_rf_confidence=0.7&sort=volume "Tallest roofs with high combined confidence" → segments=true&object_type=roof&min_confidence=0.7&sort=height_max_m "All segments by area across types" → segments=true&sort=area_sqm "All tall vegetation (tree OR shrub OR hedge) >15m" → segments=true&object_type=tree,shrub,hedge&min_height=15 "Largest building structures (roof OR greenhouse)" → segments=true&object_type=roof,greenhouse&sort=area_sqm&sort_dir=desc "Disturbance events ranked by volume" → segments=true&object_type=excavation,fill,construction,tree_loss&sort=volume #### Filter logic: AND vs OR All filters within a single query combine with AND: segments=true&object_type=tree&min_rf_confidence=0.9&min_height=20&state=Vorarlberg means: type=tree AND rf_conf>=0.9 AND height>=20m AND state=Vorarlberg Multiple types in object_type combine with OR (comma-separated): object_type=tree,shrub,hedge means: type=tree OR type=shrub OR type=hedge All other filters still AND with the type list. Full-text search (q=) tokenises by whitespace; terms are AND'd by default: q=Bregenz Gaissau matches KGs whose name/admin hierarchy contains BOTH terms. district= and gemeinde= accept names (e.g. district=Bregenz) or numeric codes (district=802). state= accepts names (e.g. state=Vorarlberg) or single-digit codes (state=8). #### Segment Power Queries (segments=true) The search index stores the top 50 individual segments per object type per KG. This enables cross-KG queries at segment level — find specific objects across all of Austria without scanning full GeoPackage files. Parameters (all optional, all AND'd together): object_type Single type or comma-separated OR list (tree,roof,shrub) min_rf_confidence Raw RF model score lower bound (0-1) max_rf_confidence Raw RF model score upper bound min_confidence Combined/calibrated score lower bound max_confidence Combined/calibrated score upper bound min_area_sqm Minimum segment area in m² max_area_sqm Maximum segment area min_height Minimum height_max_m (metres) max_height Maximum height_max_m min_volume Minimum |volume_change_m3| (absolute) max_volume Maximum |volume_change_m3| bbox Spatial filter: w,s,e,n (filters by KG bbox) state State name or code district District name or code sort Sort column: height_max_m (default), height_mean_m, area_sqm, volume (=|volume_change_m3|), rf_confidence, confidence sort_dir asc or desc (default: desc) percentile Float 0-1: return only top N% (e.g. 0.01 = top 1%) limit, offset Pagination (default limit=100, max 1000) Response: {total, offset, limit, results: [{kg_code, kg_name, object_type, lon, lat, area_sqm, height_max_m, height_mean_m, volume_change_m3, rf_confidence, confidence, rank, gemeinde_name, district_name, state_name, _links: {kg, map}}], percentile_threshold (if percentile used), percentile_count} Live examples (clickable): # Top 1% tallest trees, RF confidence > 90% https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree&min_rf_confidence=0.9&percentile=0.01 # Top 5% largest tree loss areas, RF confidence > 80% https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree_loss&min_rf_confidence=0.8&percentile=0.05&sort=area_sqm # Top 10 excavations by volume, RF confidence > 70% https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=excavation&min_rf_confidence=0.7&sort=volume&limit=10 # Tallest 5 roofs with combined confidence > 70% https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=roof&min_confidence=0.7&sort=height_max_m&limit=5 # All tall vegetation (trees OR shrubs OR hedges) above 15m, high confidence https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree,shrub,hedge&min_height=15&min_rf_confidence=0.7&sort=height_max_m # Largest building structures (roofs OR greenhouses), sorted by area https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=roof,greenhouse&sort=area_sqm&sort_dir=desc&min_confidence=0.7&limit=10 # All disturbance events (excavation/fill/construction/tree_loss) by volume https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=excavation,fill,construction,tree_loss&sort=volume&sort_dir=desc&limit=20 # Trees in Bregenz district above 30m https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree&district=Bregenz&min_height=30&sort=height_max_m # Top 5% largest crop segments with high RF confidence https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=crop&min_rf_confidence=0.8&percentile=0.05&sort=area_sqm # KG 91109 (Gaissau) full index record https://srtm-lidar-at.exe.xyz:8000/api/v1/query?kg=91109 # Aggregate stats for Vorarlberg https://srtm-lidar-at.exe.xyz:8000/api/v1/query?state=Vorarlberg&aggregate=true # Aggregate stats for district Bregenz https://srtm-lidar-at.exe.xyz:8000/api/v1/query?district=Bregenz&aggregate=true # Text search (AND of terms) https://srtm-lidar-at.exe.xyz:8000/api/v1/query?q=Bregenz+Gaissau # Point proximity (nearest KGs to a coordinate) https://srtm-lidar-at.exe.xyz:8000/api/v1/query?point=9.59,47.48 # Spatial bbox, processed only https://srtm-lidar-at.exe.xyz:8000/api/v1/query?bbox=9.55,47.46,9.62,47.50&processed_only=true #### Slow Queries (async recommended) These scan KG JSON files on disk. Use async=true to avoid timeouts. You get back a task_id immediately and poll for the result. # parcels_by_type — find individual parcels where a type has high RF confidence # Returns per-parcel results: parcel_id, centroid, elevation, type area, rf_confidence https://srtm-lidar-at.exe.xyz:8000/api/v1/query?parcels_by_type=tree&min_confidence=0.8&min_area_sqm=1500 # Same but async (recommended when many KGs processed): https://srtm-lidar-at.exe.xyz:8000/api/v1/query?parcels_by_type=tree&min_confidence=0.8&min_area_sqm=1500&async=true # → 202 {"task_id": "abc-123", "status": "running", "poll": "/api/v1/query?task_id=abc-123"} # Poll: GET /api/v1/query?task_id=abc-123 → 202 while running, 200 when done # top_features — cross-KG ranked features from JSON summaries # feature_type: trees | objects | new_buildings | infrastructure # Tallest trees across all KGs, RF confidence > 90%: https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=trees&min_confidence=0.9&async=true # New (uncadastred) buildings, confidence > 75%: https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=new_buildings&min_confidence=0.75&async=true # Infrastructure masts, high confidence: https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=infrastructure&type=mast&min_confidence=0.8&async=true # Top objects within a spatial bbox (restricts which KGs are scanned): https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=objects&bbox=9.5,47.4,9.7,47.6&min_confidence=0.7&async=true # compound filter (async) — complex multi-attribute KG search (GET or POST) https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?type_filter=tree:0.8:800&min_elevation=1000&aspect=S,SW&min_ndvi=0.5&max_buildings=10&sort=tree_canopy_sqm&sort_dir=desc&async=true # → 202 {"task_id": "def-456", ...} # Poll: /api/v1/query/compound?task_id=def-456 Speed guide: Fast (<25ms): segments=true, q=, bbox=, point=, state=, district=, type=, hansen=, aggregate= — pure index lookups Medium (seconds): compound with type_filters or landcover_filters — indexed joins Slow (sec–minutes): parcels_by_type, top_features — scan KG JSONs. Use async=true. Query modes (pick one): q= Full-text search (KG/gemeinde/district/state names) kg= Exact KG lookup (returns full index record + landcover + hansen + links) parcel= Parcel lookup (returns KG summary + parcel detail if processed) bbox= Spatial R-tree query (KGs intersecting bbox) point= Point proximity query (sorted by distance) state= KGs in a Bundesland (or aggregate if aggregate=true) district= KGs in a Bezirk (or aggregate if aggregate=true) gemeinde= KGs in a Gemeinde (or aggregate if aggregate=true) type= Rank KGs by object type (metric=area|fraction|count|height) hansen=true Hansen forest loss query (year_from, year_to, min_loss) new_buildings=true KGs with new uncadastred buildings (min_count=N) divergence=true KGs ranked by RF→final type divergence (min_divergence=N, rf_type=X, final_type=X) divergence_pairs=true Most common RF→final type mismatches across all KGs low_confidence=true KGs with lowest classification confidence (max_confidence=N) confidence_rank=asc|desc Rank KGs by classification confidence type_confidence= Rank KGs by RF confidence for a specific object type high_confidence_type= KGs where type has RF confidence >= min_confidence AND area >= min_area_sqm parcels_by_type= Per-parcel filter (async-capable): parcels where type has RF confidence >= min_confidence AND area >= min_area_sqm (scans KG JSONs) top_features= Cross-KG features (async-capable): filtered by min_confidence, optionally type=, bbox= min_confidence= Min RF confidence threshold (used with high_confidence_type, parcels_by_type, top_features) min_area_sqm= Min area m² for type (used with high_confidence_type, parcels_by_type) task_id= Poll an async query task (returns result when done, 202 while running) Modifiers: aggregate=true Return aggregate stats instead of KG list processed_only=true Only processed KGs async=true Run slow queries asynchronously (parcels_by_type, top_features) limit=N Max results (default 100, max 1000) offset=N Pagination offset (default 0) radius=N Radius in km for point query (default 5) metric= For type= ranking year_from=YYYY Start year for hansen filter year_to=YYYY End year for hansen filter Response for list queries (paginated envelope): {"total": 1234, "offset": 0, "limit": 100, "results": [{kg_code, kg_name, ...}, ...]} Iterate pages: offset=0, offset=100, offset=200, ... until offset >= total. Response for single KG (kg= or query_kg): {kg_code, kg_name, gemeinde_name, district_name, state_name, bbox, centroid, parcel_count, building_count, total_area_sqm, processed, generated_at, primary_year, dominant_type, vegetated_fraction, shannon_diversity, n_segments, elevation_{min,max,mean}_m, slope_mean_deg, aspect_dominant, ndvi_mean, tri_mean, tpi_mean, terrain_class, tree_{count,canopy_sqm,mean_height_m,stem_volume_m3}, net_volume_change_m3, temporal_stability, new_building_count, infrastructure_count, building_footprint_sqm, building_mean_height_m, building_max_height_m, building_stories_mean, building_stories_max, building_pitched_pct, new_building_footprint_sqm, new_building_mean_height_m, new_building_stories_mean, sar_vv_mean_db, sar_vh_mean_db, ndvi_harm_mean, ndvi_harm_amplitude, ndvi_harm_phase, dtm_change_mean_m, n_changed_segments, total_disturbed_volume_m3, phenology_dominant, n_tiles, building_height_coverage_pct, quality_score, quality_grade, zenodo_json_url, zenodo_json_size, zenodo_light_gpkg_url, zenodo_light_gpkg_size, zenodo_full_gpkg_url, zenodo_full_gpkg_size, zenodo_depo_id, mean_confidence, rf_classified_pct, rf_mean_confidence, rf_diverged_count, rf_diverged_pct, landcover: [{object_type, area_sqm, fraction, n_objects, height_{min,max,mean,p90}}, ...], hansen_loss: [{loss_year, loss_pixels}, ...], classification: [{object_type, rf_mean_confidence, rf_min_confidence, rf_count, diverged_count}, ...], divergence: [{rf_type, final_type, count}, ...], _links: { json: "https://srtm-lidar-at.exe.xyz:8000/api/v1/kg/", zenodo_json: "https://zenodo.org/api/files//.json", zenodo_light_gpkg: "https://zenodo.org/api/files//_light.gpkg", zenodo_full_gpkg: "https://zenodo.org/api/files//_full.gpkg" }} Zenodo links are direct download URLs for the three per-KG products: - JSON summary (~1MB): landscape analysis, landcover, parcels, buildings, terrain, NDVI, etc. - Light GPKG (~100MB): segment raster+vector, enriched parcels + buildings - Full GPKG (~400MB): all raster layers (DTM/DSM/nDSM/ortho) + segment vectors Links appear once the KG has been uploaded to Zenodo. Null otherwise. Per-parcel details (parcels.details[]): parcel_id, area_sqm, centroid, elevation_m, area_summary (type→{area_sqm,fraction}), height_distribution, vegetated_fraction, forested_fraction (tree+shrub, 0–1), dominant_type (highest-area type), is_vegetated, ndsm_max_m, ndsm_mean_m, classification, hansen_loss: {total_pixels, recent_5yr_pixels, by_year: {"2001": N, ...}}, elevation_min_m, elevation_max_m, elevation_range_m, slope_mean_deg, aspect_mean_deg, aspect_dominant, tri_mean, tpi_mean, terrain_class, vertex_heights: [{lat, lon, dtm_m}, ...] (AMS anchor points at every boundary vertex), frav: {type_letter: area_sqm_int, ...} (Fraction-Area Vector — every parcel), top_objs: [[type_letter, hmax, hmean, area, lon, lat, conf, rf_conf, manmade], ...] (compact, 5 entries, ~60B each — see TYPE_LETTER table below), top_trees: [[hmax, hmean, hp90, area, lon, lat, ndvi_mean, ndvi_fused, height_change_m, phenology_class, conf, rf_conf], ...] (5 entries). TYPE_LETTER (single-char): t=tree s=shrub g=grass h=hedge w=water R=roof G=greenhouse P=solar_panel F=fence W=wall M=mast T=wind_turbine X=substation r=road p=path k=parking b=bridge c=crop o=orchard v=vineyard a=garden B=bare_soil K=rock E=excavation L=fill l=tree_loss C=construction e=earthwork u=unclassified. Lowercase = natural, uppercase = man-made. Per-building details (building_footprints.details[]): building_id, footprint_area_sqm, centroid, centroid_dtm_m, max_height_m, mean_height_m, dsm_std, roof_type_hint, stories_est, segment_types, ns, full_address, house_number, street, postal_code, location, footprint_area_sqm, centroid_lon, centroid_lat, vertex_heights: [{lat, lon, dtm_m, obj_height_m}, ...] (AMS anchor points), classification. classification dict (per-parcel/building): rf_classified, rules_classified, total_segments, mean_confidence, rf_mean_confidence, diverged_count, diverged_pct, divergences (top 5 [{rf_type, final_type, count}]), by_type: {: {segments, area_sqm, mean_confidence, rf_count, rules_count, rf_mean_confidence, diverged_count}}. Response for aggregate (state/district/gemeinde/country): {name, level, total_kgs, processed_kgs, total_area_km2, total_parcels, total_buildings, total_trees, tree_canopy_km2, total_new_buildings, avg_elevation_m, avg_slope_deg, avg_ndvi, avg_quality_score, top_landcover: [...], hansen_loss_by_year: {...}} #### GET|POST /api/v1/query/compound ← COMPOUND FILTER Filter KGs by any combination of attributes. Accepts GET (query params) or POST (JSON body). Returns paginated: {total, offset, limit, results}. Supports async: async=true param. Poll with task_id= param. GET param syntax: - Flat params: state=Vorarlberg&min_slope=15&max_buildings=0 - bbox=w,s,e,n aspect=S,SW,W (comma-separated) - type_filter=tree:0.8:800 (repeatable, type:min_confidence:min_area_sqm) - landcover_filter=grass:1300:0.1 (repeatable, type:min_area_sqm:min_fraction) - limit, offset, async, task_id as query params Filter keys (all optional): bbox: [w,s,e,n] spatial filter state, district, gemeinde: str admin filter aspect: ["S","SW","W"] dominant aspect direction dominant_type, phenology, quality_grade, terrain_class: str exact match terrain_class values: level, nearly_level, slightly_rugged, intermediately_rugged, moderately_rugged, highly_rugged, extremely_rugged Numeric ranges (prefix min_ or max_): --- Terrain --- slope slope_mean_deg roughness roughness_mean elevation elevation_mean_m elevation_min elevation_min_m (valley floor) elevation_max elevation_max_m (ridge top) elevation_range elevation_range_m steepness_max steepness_max_deg tri Terrain Ruggedness Index --- Area / parcels / segments --- total_area total_area_sqm (KG area) parcels parcel_count segments n_segments --- Buildings --- buildings building_count new_buildings new_building_count (min only) infrastructure infrastructure_count (min only) building_height building_mean_height_m building_max_height building_max_height_m building_stories building_stories_mean building_stories_max building_stories_max (tallest building) building_pitched_pct % pitched roofs (0-100) building_footprint total building_footprint_sqm new_building_footprint new_building_footprint_sqm (min only) new_building_height new_building_mean_height_m (min only) new_building_stories new_building_stories_mean (min only) building_height_coverage building_height_coverage_pct (min only) --- Trees --- tree_count tree_count (min only) tree_height tree_mean_height_m (min only) tree_canopy_sqm tree_canopy_sqm (min only) tree_volume tree_stem_volume_m3 (min only) --- Vegetation --- ndvi ndvi_mean vegetated_fraction vegetated_fraction (0-1) shannon_diversity shannon_diversity (min only) --- NDVI harmonics --- ndvi_amplitude ndvi_harm_amplitude (min only) ndvi_harm_mean ndvi_harm_mean (annual mean) ndvi_phase ndvi_harm_phase (peak month) --- SAR --- sar_vv sar_vv_mean_db sar_vh sar_vh_mean_db --- Temporal change --- dtm_change dtm_change_mean_m volume_change net_volume_change_m3 changed_segments n_changed_segments (min only) disturbed_volume total_disturbed_volume_m3 (min only) temporal_stability temporal_stability --- Classification quality --- confidence mean_confidence rf_confidence rf_mean_confidence diverged_pct rf_diverged_pct (max only) rf_diverged_count rf_diverged_count (max only) rf_classified_pct rf_classified_pct (min only) quality_score quality_score type_filters: [{type, min_confidence, min_area_sqm}, ...] RF classification confidence + area per object type (joins kg_classification) landcover_filters: [{type, min_area_sqm, min_fraction, min_height_mean, max_height_mean}, ...] Landcover area/fraction/height per type (joins kg_landcover) sort: str (column name), sort_dir: "asc"|"desc" limit: int (default 50, max 1000), offset: int async: true Run query asynchronously (returns task_id to poll) task_id: str Poll an async compound query All filters are fast (pure SQL on indexed columns). Returns {total, offset, limit, results}. ### Search Index Schema kg table columns (all indexed): kg_code, kg_name, gemeinde_code, gemeinde_name, district_code, district_name, state_code, state_name, bbox, centroid, parcel_count, building_count, total_area_sqm, processed, primary_year, dominant_type, vegetated_fraction, shannon_diversity, n_segments, elevation_min_m, elevation_max_m, elevation_mean_m, slope_mean_deg, aspect_dominant, roughness_mean, steepness_max_deg, elevation_range_m, tri_mean, terrain_class, ndvi_mean, tree_count, tree_canopy_sqm, tree_mean_height_m, tree_stem_volume_m3, net_volume_change_m3, temporal_stability, new_building_count, infrastructure_count, building_footprint_sqm, building_mean_height_m, building_max_height_m, building_stories_mean, building_stories_max, building_pitched_pct, new_building_footprint_sqm, new_building_mean_height_m, new_building_stories_mean, sar_vv_mean_db, sar_vh_mean_db, ndvi_harm_mean, ndvi_harm_amplitude, ndvi_harm_phase, dtm_change_mean_m, n_changed_segments, total_disturbed_volume_m3, phenology_dominant, n_tiles, building_height_coverage_pct, mean_confidence, rf_classified_pct, rf_mean_confidence, rf_diverged_count, rf_diverged_pct, quality_score, quality_grade, zenodo_json_url, zenodo_light_gpkg_url, zenodo_full_gpkg_url kg_buildings table (per-building records, joined via kg_code): building_id, roof_type_hint (flat/pitched), max_height_m, mean_height_m, stories_est, footprint_area_sqm, centroid_lon, centroid_lat, centroid_dtm_m Async flow: 1. POST {"min_elevation": 1500, "async": true} → 202 {task_id, status, poll} 2. POST {"task_id": "..."} → 202 while running, 200 with result when done Example: tree conf>0.8 area>800 + grass conf>0.8 area>1300 + no buildings + SW aspect + rugged: POST {"type_filters":[{"type":"tree","min_confidence":0.8,"min_area_sqm":800}, {"type":"grass","min_confidence":0.8,"min_area_sqm":1300}], "max_buildings":0, "aspect":["S","SW","W"], "min_roughness":2.0} #### GET /api/v1/query/progress?task_id= Poll an async query task. Returns result directly when done. 202 {active, step, detail, elapsed} while running 200 {total, offset, limit, results} when complete (result delivered inline) 500 {error} on failure #### GET /api/v1/kg/ Full KG JSON summary if processed locally, else index data with Zenodo links. Add ?index_only=true to always use index (faster, no file I/O). #### GET /api/v1/parcel/ Parcel lookup via search index + local JSON detail. #### GET /api/v1/kg//buildings Height-enriched building footprints (GeoJSON features from light GPKG). Lazy-loads from Zenodo on first access, cached locally (1GB LRU). Properties: building_id, ns, full_address, house_number, street, postal_code, location, max_height_m, mean_height_m, dsm_std, roof_type_hint (flat/pitched), stories_est, footprint_area_sqm, centroid_lon, centroid_lat, centroid_dtm_m, vertex_heights (pipe-separated "lat,lon,dtm_m,obj_height_m|..." in GPKG; array of {lat, lon, dtm_m, obj_height_m} in JSON). Params: bbox=w,s,e,n limit=N(default 500, max 5000) offset=N #### GET /api/v1/kg//new_buildings Detected new buildings not in cadastre (from light GPKG). Properties: type, area_sqm, max_height_m, stories_est, roof_type_hint, confidence. Params: bbox=w,s,e,n limit=N offset=N #### GET /api/v1/kg//heightfield ← FAST (<100 ms) 2.5D terrain for games / viz Index-only gridded DTM estimate for a whole KG — no raster read, no GPKG. IDW (k nearest, 3 km cutoff) over the pre-indexed sample points: parcel centroids (`kg_parcels.elevation_m`, ~55 m spacing) + building centroids (`kg_buildings.centroid_dtm_m`, dense in settlements). Parcel-scale accuracy — fine for isometric relief / hillshade / extrusion offsets, NOT for survey use (use POST /api/v1/lidar/geotiff for real 1 m DTM). Params: cell=N grid spacing in metres (default 50, min 20, max 500) k=N IDW neighbours (default 8) Returns: {kg_code, bbox:[w,s,e,n], cell_m, cols, rows, origin:'nw', rows_major_north_to_south:true, z:[[…]] (row 0 = north edge, null = no sample within 3 km), elev_min, elev_max, kg_elev_min, kg_elev_max (true DTM extremes from the KG index), samples, source} Grid cell (r,c) → lon = w + c*cell/(111320*cos(lat)), lat = n - r*cell/110540. ~90 KB at cell=50 for a typical KG; cached 1 h server-side. 404 with processed:false for KGs not yet processed. Recipe — everything a 2.5D renderer needs for one KG, all <100 ms each: /api/v1/kg//heightfield?cell=50 terrain grid /api/v1/query/parcels?kg=&limit=5000 per-parcel elev/slope/aspect/ndsm_max/forested_fraction /api/v1/buildings/search?kg=&limit=5000 building centroid, dtm, max_height, stories, roof type /api/v1/query?segments=true&kg=&object_type=tree,hedge,mast,rock&sort=height_max_m&limit=50 tallest objects per type with lon/lat/height AVOID for interactive use: query/parcels?bbox= (no spatial index, seconds), segments=true&bbox= (1–5 s), /kg//segments|buildings (GPKG fetch), and every POST raster endpoint (10–60 s). #### GET /api/v1/buildings/search Fast building search across the index (no GPKG needed). Queries the kg_buildings table. Params: kg=CODE Filter to a single KG min_height=N max_height_m >= N max_height=N max_height_m <= N min_stories=N stories_est >= N max_stories=N stories_est <= N roof_type=flat|pitched exact match on roof_type_hint min_area=N footprint_area_sqm >= N max_area=N footprint_area_sqm <= N limit=N (default 500, max 5000) offset=N Returns: {total, offset, limit, results: [{kg_code, building_id, ns, roof_type_hint, max_height_m, mean_height_m, stories_est, footprint_area_sqm, centroid_lon, centroid_lat, centroid_dtm_m}, ...]} Speed: fast (<25ms), pure index lookup. #### GET /api/v1/kg//infrastructure Detected infrastructure features (from light GPKG). Properties: type, area_sqm, volume_m3, max_height_m, est_parking_spots, confidence. Params: bbox=w,s,e,n limit=N offset=N #### GET /api/v1/kg//segments Segment polygons with classification (from light GPKG). Properties: id, type, type_code, group_type, height_class, area_sqm, aspect_mean_deg, aspect_dominant, elevation_mean_m, elevation_min_m, elevation_max_m, tri_mean, tpi_mean, curvature_mean, terrain_class, etc. Params: bbox=w,s,e,n type=tree,roof,... limit=N offset=N #### GET /api/v1/kg//layers List available vector layers in the KG's GPKG. Params: variant=light|full (default: light) --- ## 25 Object Types | Code | Type | Detection | |------|------|-----------| | 1 | tree | nDSM>4m, rough DSM, high NDVI | | 2 | shrub | nDSM 0.5–4m, high NDVI | | 3 | grass | Ground level, moderate+ NDVI, smooth DTM | | 4 | hedge | Elongated shrub (length/width>4) | | 5 | water | ESA water class, very low NDVI+NIR, flat | | 10 | roof | Compact elevated, smooth DSM, low NDVI | | 11 | greenhouse | Roof-like, high NIR transmittance | | 12 | solar_panel | Very smooth, bright, low NDVI on roof | | 15 | fence | Low (0.5–2m), thin, elongated | | 16 | wall | Narrow elevated, adjacent to roof | | 17 | mast | Tiny footprint (<10m²), very tall (>15m) | | 20 | road | Smooth DTM (<0.04m roughness), elongated, low NDVI | | 21 | path | Narrower road (<3m width) | | 22 | parking | Smooth, large, compact, low NDVI | | 23 | bridge | Elevated road/path over gap | | 30 | crop | Flat, seasonal NDVI, ESA cropland | | 31 | orchard | Regular tree spacing, <10m height | | 32 | vineyard | Low rows, <3m, row pattern | | 33 | garden | Mixed vegetation near buildings | | 40 | bare_soil | Low NDVI, flat-to-moderate slope | | 41 | rock | Steep + very rough DTM + low NDVI | | 50 | excavation | DTM lowered >0.20m between dates (≥25m²) | | 51 | fill | DTM raised >0.20m between dates (≥25m²) | | 52 | tree_loss | nDSM dropped >2m, terrain intact (≥8m²) | | 53 | construction | New structure or site clearing | ## 11 Group Types | Code | Group | Members | |------|-------|---------| | 101 | forest | tree + shrub + hedge | | 102 | woodland | shrub + hedge (sparse) | | 103 | hedgerow | hedge + hedge | | 106 | waterbody | water + water | | 110 | building | roof + wall + solar_panel + greenhouse | | 115 | road_network | road + path + parking | | 120 | cropland | crop + grass | | 121 | pasture | grass + garden | | 122 | orchard_grove | orchard + vineyard | | 130 | quarry | excavation + fill | | 131 | construction_site | construction + excavation + fill | --- ## One-Stop URL (# url) GET /api/v1/onestop — Trigger segmentation + download from a single bookmarkable URL. Designed for users on limited/mobile connections. No POST body needed. Query params: bbox=lon_min,lat_min,lon_max,lat_max Bounding box (required) name=MySave Save name / share ID (slug, 1-80 chars, [A-Za-z0-9_-]) min_object_size=10 Min segment area in m² (default: 10) include_ortho=true Orthophoto features (default: true) include_temporal=false Multi-date comparison include_copernicus=false Sentinel-2/SAR include_cadastre=false Cadastre ground truth include_hansen=false Hansen forest change types=tree,road Object type filter height_min=X Height filter: >= X metres height_max=X Height filter: <= X metres height_op=gt|lt|between Height operator (inferred if omitted) format=json|gpkg|kml Output format (default: json) layers=segments GPKG layers (default: segments with vector) include_segments_vector=true Include vector polygon layer in GPKG segment_geometry=point|polygon Feature geometry (default: point for KML, polygon for GPKG) segment_geometry_style=type|height Colour scheme: by object type or viridis height ramp group_by=type|height_class KML folder grouping Flow: 1. GET /api/v1/onestop?bbox=...&format=gpkg → 202 {task_id, poll_url, estimated_seconds} 2. Poll the poll_url (same endpoint + task_id) until status=done 3. Final poll returns the file download (GPKG/KML) or JSON result Result is auto-saved as a share for later access (auto_share_id in response). GPKG contents (format=gpkg, defaults): Raster tables: segment_type, segment_height (float32, EPSG:3035); add include_dtm=true for DTM/DSM/nDSM; layers=... for ortho/overlays. Vector layer 'segments' (MultiPolygon, EPSG:3035) — the FULL 55-attribute schema, identical to the Zenodo {kg}_full/_light GPKGs (see "GPKG segments layer" under Austria Processor above): identity (type, type_code, group_type, height_class), shape (area_sqm, perimeter_m, compactness, elongation, solidity, extent), height (max/mean/p90/std), terrain (slope, aspect, roughness, dsm_edge_strength, elevation min/mean/max, tri, tpi, curvature, terrain_class), spectral (ndvi_mean/std/fused, brightness, nir), temporal (height_change_m, dtm_change_m, temporal_stability, volume_change_m3, volume_change_abs_m3, dtm_change_max_m), texture (glcm_entropy, glcm_homogeneity, texture_complexity), SAR (sar_vv, sar_vh), phenology (harm_amplitude, harm_phase, phenology_class), classification provenance (confidence, is_manmade, classifier, rf_model, rf_type, rf_confidence), rendering (color, color_height), obs_year. Note: temporal/SAR/phenology fields are 0 unless include_temporal / include_copernicus were set on the analysis. Vector layer 'segment_points' (Point, EPSG:4326) — centroid per object, same attributes (54 props, no height_std duplicate), mirrors the API GeoJSON point features. layer_styles table — QGIS auto-render styles for both vector layers. Already-processed KGs: if your bbox lies inside a processed KG, prefer the prebuilt Zenodo products — zenodo_light_gpkg_url / zenodo_full_gpkg_url from GET /api/v1/kg/ or /api/v1/query — over re-running onestop. Processing queue: max 2 concurrent tasks, 4 in queue. Returns 503 if full. ### Timing Estimates (bounding box < 1 km²) | Configuration | Est. time | Notes | |---------------------------|-----------|----------------------------------| | ortho only (default) | ~30-60s | DTM+DSM+ortho, fast | | ortho + temporal | ~60-90s | +3-date comparison | | ortho + temporal + all | ~90-120s | +Copernicus+cadastre+Hansen | | GPKG/KML export | +5-10s | Added on top of analysis time | | Auto-save (always happens) | +1-2s | Gzip-compressed share saved | For 0.5 km² with ortho, expect ~40s total. For 0.1 km², ~25s. On mobile connections, GPKG download (1-5 MB) may add 5-30s depending on speed. ### Example One-Stop URLs (< 1 km²) ```bash # 1. Trees >30m in a forest area near Graz (~0.3 km², ~35s, JSON) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&height_min=30&format=json' # → {"task_id":"...","status":"running","poll_url":"...","estimated_seconds":35} # 2. Same area, download as GeoPackage with vector polygons (~40s + 5s export) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&height_min=30&format=gpkg' # Poll the poll_url until done, then the final request returns the .gpkg file # 3. All objects in a small village (~0.2 km², ortho+temporal, KML, ~50s) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.35,47.15,15.355,47.154&include_temporal=true&format=kml' # 4. Buildings only, height 5-15m, as GPKG (~30s) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=roof&height_min=5&height_max=15&format=gpkg' # 5. Poll a running task (same URL with task_id) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?task_id=&format=gpkg' # → 202 while running, then file download when done # 6. Full pipeline in a shell script: TASK=$(curl -s 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&height_min=30&format=gpkg' | jq -r .task_id) while true; do RESP=$(curl -s -w '%{http_code}' -o /tmp/result.gpkg "https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?task_id=$TASK&format=gpkg") [ "$RESP" = "200" ] && echo "Downloaded!" && break sleep 5 done ``` ### Segment Geometry Examples (polygon vs point, style modes) ```bash # KML with polygon outlines coloured by height (viridis ramp) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&format=kml&segment_geometry=polygon&segment_geometry_style=height&group_by=height_class' # KML with polygons coloured by object type (default style) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=kml&segment_geometry=polygon' # KML with point centroids only (default, fast — no vectorisation) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=kml' # GPKG with height-coloured polygons (segment_geometry defaults to polygon for GPKG) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=gpkg&segment_geometry_style=height' # GPKG with points only (skip vectorisation, smaller file) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=gpkg&segment_geometry=point' ``` ### Height Filter Examples (work on GPKG, KML, and one-stop) ```bash # Trees taller than 30m (height_min=30, operator inferred as >=) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&height_min=30&format=gpkg' # Objects shorter than 5m curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&height_max=5&format=kml' # Objects between 10m and 25m curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&height_min=10&height_max=25&format=gpkg' # Height filter on share GPKG download curl -o filtered.gpkg 'https://srtm-lidar-at.exe.xyz:8000/api/v1/share/Kohlschwarz80/download.gpkg?types=tree&height_min=20' ``` --- ## Examples ```bash # Async segmentation with all sources curl -X POST 'https://srtm-lidar-at.exe.xyz:8000/api/v1/segment?async=true&include_ortho=true&include_temporal=true&include_copernicus=true&include_cadastre=true' \ -H 'Content-Type: application/json' \ -d '{"geometry":{"type":"Polygon","coordinates":[[[15.4,47.07],[15.403,47.07],[15.403,47.073],[15.4,47.073],[15.4,47.07]]]}}' # → {"task_id":"...","status":"running"} # Poll progress curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/segment/progress?task_id=...' # Get result curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/segment/result?task_id=...' # Layer availability curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/layers?bbox=15.1,47.1,15.2,47.2' # Change detection curl -X POST 'https://srtm-lidar-at.exe.xyz:8000/api/v1/changes' \ -H 'Content-Type: application/json' \ -d '{"date_a":"20220915","date_b":"20240915","geometry":{"type":"Polygon","coordinates":[[[15.4,47.07],[15.41,47.07],[15.41,47.08],[15.4,47.08],[15.4,47.07]]]}}' # List shares curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/shares?limit=10' # Load share (follows rename redirects) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/share/Kohlschwarz80' # Direct GPKG download from share curl -o analysis.gpkg 'https://srtm-lidar-at.exe.xyz:8000/api/v1/share/Kohlschwarz80/download.gpkg' # --- Search Index queries (all list responses are paginated: {total, offset, limit, results}) --- # Index status curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/index/status' # Text search with pagination curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?q=Gaissau' # → {"total": 3, "offset": 0, "limit": 100, "results": [...]} curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?q=Wien&limit=10&offset=0' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?q=Wien&limit=10&offset=10' # page 2 # Spatial queries (paginated) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?bbox=15.0,47.0,16.0,48.0&limit=50&offset=0' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?point=9.59,47.48&radius=5' # Admin hierarchy curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?state=Vorarlberg' # list KGs curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?state=Vorarlberg&aggregate=true' # stats curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?district=802' # Bregenz district KGs curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?district=802&aggregate=true' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?aggregate=true' # all Austria # Specific KG detail (landcover + hansen + zenodo download links) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?kg=91109' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/kg/91109?index_only=true' # → includes zenodo_json_url, zenodo_light_gpkg_url, zenodo_full_gpkg_url # and _links.zenodo_json, _links.zenodo_light_gpkg, _links.zenodo_full_gpkg # (direct Zenodo download URLs when available) # Rebuild index (picks up new processor results + Zenodo manifest) curl -X POST 'https://srtm-lidar-at.exe.xyz:8000/api/v1/index/rebuild' # Parcel lookup curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?parcel=91109-535/6' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcel/91109-535/6' # Object type rankings across KGs curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?type=tree&metric=area&limit=10' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?type=roof&metric=fraction' # Hansen forest loss curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?hansen=true&year_from=2020&year_to=2024' # New buildings curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?new_buildings=true&min_count=50' # List processed KGs curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?processed_only=true' # Classification divergence — KGs where RF and final types disagree most curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?divergence=true&limit=20' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?divergence=true&rf_type=grass&final_type=crop' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?divergence=true&min_divergence=10' # Most common divergence pairs across all KGs curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?divergence_pairs=true' # Classification confidence — lowest confidence KGs curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?low_confidence=true&max_confidence=0.4' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?confidence_rank=asc&limit=20' # RF confidence for a specific type curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?type_confidence=tree&limit=20' # --- RF Classification Queries --- # KGs where tree RF confidence > 0.8 and tree area > 1500 m² curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?high_confidence_type=tree&min_confidence=0.8&min_area_sqm=1500' # Individual parcels where tree RF confidence > 0.8 and tree area > 1500 m² curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?parcels_by_type=tree&min_confidence=0.8&min_area_sqm=1500' # Tallest trees across all KGs with RF confidence > 0.9 curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=trees&min_confidence=0.9' # New building footprints with RF roof confidence > 0.75 curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=new_buildings&min_confidence=0.75' # New building footprints in a specific area (bbox) with RF conf > 0.75 curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=new_buildings&min_confidence=0.75&bbox=15.4,47.0,15.5,47.1' # Infrastructure (e.g. masts) with RF confidence > 0.8 curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=infrastructure&type=mast&min_confidence=0.8' # --- Compound Query (GET|POST /api/v1/query/compound) --- # All filters combined in one request. Pure SQL on indexed columns, <25ms. # GET query params or POST JSON body → {total, offset, limit, results} # GET syntax: type_filter=type:confidence:area (repeatable), landcover_filter=type:area:fraction (repeatable) # aspect=S,SW,W (comma-separated), bbox=w,s,e,n, all min_*/max_* as flat params # ── Terrain & Elevation ── # High alpine: valley floor > 1500m, ridge top > 2500m, elevation range > 800m curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_elevation_min=1500&min_elevation_max=2500&min_elevation_range=800&terrain_class=highly_rugged' # Steep forested slopes: slope > 15°, 1000+ trees, sorted by canopy area curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_slope=15&min_tree_count=1000&sort=tree_canopy_sqm&sort_dir=desc' # Flat agricultural lowlands: level terrain, low elevation, many parcels curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?terrain_class=level&max_elevation=300&min_parcels=200&min_total_area=5000000' # ── Buildings & Urban ── # Dense urban: 500+ buildings, multi-story (mean > 3, tallest > 5), large footprint curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_buildings=500&min_building_stories=3&min_building_stories_max=5&min_building_footprint=50000&sort=building_max_height_m&sort_dir=desc' # Tall buildings with pitched roofs: max height > 20m, > 30% pitched curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_building_max_height=20&min_building_pitched_pct=30' # New construction hotspots: 3+ new buildings, > 500m² footprint, > 5m height curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_new_buildings=3&min_new_building_footprint=500&min_new_building_height=5' # Rural character: no tall buildings, small footprint, mostly pitched roofs curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?max_building_stories_max=2&max_building_footprint=10000&min_building_pitched_pct=80' # ── Trees & Vegetation ── # Forest-rich with high biomass: 1000+ trees, stem volume > 10000 m³, seasonal NDVI curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_tree_count=1000&min_tree_volume=10000&min_ndvi_amplitude=0.15&sort=tree_stem_volume_m3&sort_dir=desc' # High biodiversity: Shannon > 2.0, NDVI > 0.4, > 60% vegetated curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_shannon_diversity=2.0&min_ndvi=0.4&min_vegetated_fraction=0.6' # Per-type classification: tree confidence > 0.8, area > 800m² + grass > 1300m² curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?type_filter=tree:0.8:800&type_filter=grass:0.8:1300' # Landcover: > 20% crop area, mean crop height < 1m curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?landcover_filter=crop:0:0.2:0:1.0' # ── SAR & Phenology ── # High SAR + evergreen in Tirol (conifer forests) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?state=Tirol&min_ndvi=0.6&max_sar_vv=-10&phenology=evergreen' # Late-season NDVI peak (phase 7-9 = July-September), high mean NDVI curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_ndvi_phase=7&max_ndvi_phase=9&min_ndvi_harm_mean=0.5' # ── Temporal Change & Disturbance ── # Active earthworks: high volume change + many changed segments curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_changed_segments=20&min_disturbed_volume=5000&sort=total_disturbed_volume_m3&sort_dir=desc' # Erosion/subsidence: negative volume change, sorted ascending curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?max_volume_change=-1000&sort=net_volume_change_m3&sort_dir=asc' # ── Classification Quality ── # High quality: RF > 90% classified, low divergence, quality score > 80 curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_rf_classified_pct=90&max_rf_diverged_count=5&min_quality_score=80' # ── Terrain Deep Dive ── # Full terrain filter: level terrain, low slope, low TRI, NE aspect, 350-500m curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?terrain_class=level&max_slope=3&max_tri=0.5&aspect=NE&min_elevation=350&max_elevation=500&min_vegetated_fraction=0.5&min_buildings=100' # Steep SE-facing alpine meadows: slope > 20°, SE/S aspect, elevation 1000-2000m, high NDVI curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_slope=20&aspect=SE,S&min_elevation=1000&max_elevation=2000&min_ndvi=0.5&min_vegetated_fraction=0.7' # Valley floors: low elevation_min, high elevation_range (deep valleys), flat terrain curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?max_elevation_min=400&min_elevation_range=500&terrain_class=level' # ── The Kitchen Sink: multi-domain compound ── # Wilderness candidates: no buildings, rugged, high trees, SW-facing, high NDVI, good RF curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?max_buildings=0&aspect=S,SW,W&min_roughness=2.0&min_tree_count=100&min_ndvi=0.5&min_confidence=0.6&type_filter=tree:0.8:800' # Urban + green: 200+ buildings, 500+ trees, high Shannon diversity, good quality curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_buildings=200&min_tree_count=500&min_shannon_diversity=2.0&min_quality_score=0.8&sort=shannon_diversity&sort_dir=desc' # ── Pagination ── # Page through compound results (25 per page) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_elevation=1000&limit=25&offset=0' # → {"total": 847, "offset": 0, "limit": 25, "results": [...]} # ── Async ── # Async compound query (for very complex filter sets) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_elevation=1500&async=true' # → 202 {"task_id": "...", "status": "running", "poll": "/api/v1/query/compound"} # Poll: /api/v1/query/compound?task_id= ``` ```bash # --- Per-Parcel Index Query (GET /api/v1/query/parcels) --- # Direct SQL on kg_parcels table. Returns individual parcels, <50ms. # Combines terrain, vegetation, classification, Hansen, and building attributes. # ── The example query: "Zeig mir alle Parzellen auf denen ein Haus mit nur einem # Stockwerk und einem Spitzdach steht, die eine Südhangneigung von über 30% haben # und kleiner als 5000 Quadratmeter ab einer Seehöhe von 900 Metern, mit wenig # Entwaldung in den letzten 5 Jahren." ── # (30% slope ≈ 17°) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?building_roof_type=pitched&building_max_stories=1&aspect=S,SE,SW&min_slope=17&max_area=5000&min_elevation=900&max_hansen_recent_5yr=5&sort=elevation_m&sort_dir=desc&limit=100' # Highly forested parcels above 1500m with no recent deforestation curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?min_forested_fraction=0.7&min_elevation=1500&max_hansen_recent_5yr=0&sort=forested_fraction&sort_dir=desc' # Large grassy parcels in Vorarlberg curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?state=Vorarlberg&dominant_type=grass&min_area=10000&sort=area_sqm&sort_dir=desc' # Steep rocky terrain without vegetation (quarries, cliffs) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?dominant_type=rock&min_slope=25&max_vegetated_fraction=0.1&sort=slope_mean_deg&sort_dir=desc' # Flat low-elevation parcels with high vegetation and buildings (suburban gardens) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?terrain_class=level&max_elevation=400&min_vegetated_fraction=0.6&building_min_stories=1&sort=vegetated_fraction&sort_dir=desc' # Parcels with significant recent Hansen forest loss (monitoring deforestation) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?min_hansen_recent_5yr=20&min_forested_fraction=0.3&sort=hansen_recent_5yr_pixels&sort_dir=desc' ``` --- ## Cross-API Bridge (Cadastre + Landscape) These endpoints combine Austrian cadastral data (parcels, buildings, legal refs, protected areas) with landscape analysis data (NDVI, vegetation, RF classification, elevation, Hansen forest loss) for nature conservation assessment and parcel purchase opportunity analysis. The cadastre API at https://cadastre-process-api.exe.xyz provides land registry, legal references, and protected areas. Our API provides remote sensing analysis. The bridge endpoints join them automatically. ### GET /api/v1/lookup Proxy to cadastre EDM lookup — diacritics-insensitive search across Austria's federal register (Gemeinden, KGs, Ortschaften, PLZ). Params: q (required), type (plz|gemeinde|kg|ortschaft), limit (default 20) Example: /api/v1/lookup?q=Kofla → finds "Köflach" Example: /api/v1/lookup?q=8580 → finds PLZ 8580 entries ### GET /api/v1/query/parcels ← FAST PER-PARCEL INDEX QUERY Direct SQL query over the kg_parcels index table. No JSON loading or GPKG access. Returns parcels matching attribute + building + spatial filters in <50ms. Parcel attribute filters: kg= Filter by KG code state= Bundesland filter district= Bezirk filter gemeinde= Gemeinde filter bbox= Spatial bbox min_area / max_area Parcel area (m²) min_elevation / max_elevation Elevation (m) min_slope / max_slope Slope (degrees) min_tri / max_tri Terrain Roughness Index terrain_class= level|nearly_level|slightly_rugged|intermediately_rugged|moderately_rugged|highly_rugged|extremely_rugged aspect= Comma-separated aspect directions dominant_type= Dominant segment type (tree/grass/roof/crop/...) min_vegetated_fraction / max_vegetated_fraction 0–1 min_forested_fraction / max_forested_fraction 0–1 (tree+shrub only) min_ndsm_max / max_ndsm_max Max nDSM height on parcel (m) is_vegetated=true/false Boolean vegetation flag min_confidence / min_rf_confidence Classification confidence thresholds max_hansen_recent_5yr / min_hansen_recent_5yr Hansen loss pixels (2020–2024) max_hansen_total / min_hansen_total Hansen loss pixels (all years) building_roof_type=pitched/flat Parcel must contain a building with this roof type building_min_stories / building_max_stories Building stories filter (any building in parcel) --- Per-parcel building rollup (computed via PIP at index build) --- has_buildings=true/false Parcel contains ≥1 building min_buildings / max_buildings Number of buildings on parcel min_building_height / max_building_height Tallest building height (m) min_building_stories / max_building_stories Tallest building stories --- Auto-classification (per-parcel landscape label, from frav + terrain + buildings + hansen) --- auto_class= Comma-separated allowed. Classes: forest, young_forest, wooded, meadow, alpine_meadow, cropland, vineyard, orchard, shrubland, built_up, farmstead, infrastructure, water_body, disturbance, bare, mixed auto_subclass= multi_storey | apartments | with_house | recently_thinned | regenerating | pasture | rugged | dense | recent_clearfell | tall_forest | open_woodland | tree_loss | construction | earthwork | orchard_meadow | high_alpine | ... min_auto_class_confidence=0..1 Minimum classifier confidence (0.5–1.0) sort= Sort: elevation_m|area_sqm|slope_mean_deg|tri_mean|vegetated_fraction|forested_fraction|ndsm_max_m|hansen_recent_5yr_pixels|hansen_total_pixels|mean_confidence|rf_mean_confidence|building_count|building_max_height_m|building_max_stories|auto_class_confidence sort_dir=asc/desc Default: desc limit / offset Pagination (default 100, max 1000) Returns: {total, offset, limit, results: [{kg_code, parcel_id, area_sqm, centroid_lon, centroid_lat, elevation_m, slope_mean_deg, terrain_class, aspect_dominant, vegetated_fraction, forested_fraction, dominant_type, ndsm_max_m, hansen_total_pixels, hansen_recent_5yr_pixels, ...}]} Examples: ``` # 1-storey pitched-roof house, south slope >17°, <5000m², >900m, low recent deforestation curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?building_roof_type=pitched&building_max_stories=1&aspect=S,SE,SW&min_slope=17&max_area=5000&min_elevation=900&max_hansen_recent_5yr=5&sort=elevation_m&sort_dir=desc&limit=100' # Highly forested parcels above 1500m with little disturbance curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?min_forested_fraction=0.7&min_elevation=1500&max_hansen_total=10&sort=forested_fraction&sort_dir=desc' # Large grassy parcels in Vorarlberg curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?state=Vorarlberg&dominant_type=grass&min_area=10000&sort=area_sqm&sort_dir=desc' # === Auto-classification queries (Apr-2026) === # All recently-thinned forests (Hansen 2020+ loss inside an otherwise tree-dominated parcel) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_class=forest&auto_subclass=recently_thinned&sort=hansen_recent_5yr_pixels&sort_dir=desc&limit=50' # Apartment blocks (auto-classed built_up with tallest building ≥5 stories) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_subclass=apartments&min_building_stories=5&sort=building_max_height_m&sort_dir=desc' # Working farmsteads (with house + agricultural surroundings) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_class=farmstead&auto_subclass=with_house&min_area=2000&limit=100' # Single-house parcels above 900m on south-facing slopes >17° (alpine chalets) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?has_buildings=true&max_buildings=1&max_building_stories=2&aspect=S,SE,SW&min_slope=17&min_elevation=900&sort=elevation_m&sort_dir=desc' # Recent clearfells (≥20% of parcel cleared in last 5 years, no longer tree-dominated) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_subclass=recent_clearfell&sort=hansen_recent_5yr_pixels&sort_dir=desc' # Young / regenerating forest above 1000m (mean tree height <8m) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_class=young_forest&min_elevation=1000&sort=elevation_m&sort_dir=desc' # Mixed-use parcels in Vienna (auto_class=mixed, where the dominant signal isn't decisive) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?state=Wien&auto_class=mixed&min_auto_class_confidence=0.4' # Cropland / orchard / vineyard combined ("productive agriculture") curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_class=cropland,vineyard,orchard&min_area=1000&sort=area_sqm&sort_dir=desc' # Disturbance map: any parcel currently flagged as construction/earthwork/clearfell curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_class=disturbance&sort=area_sqm&sort_dir=desc&limit=200' # Rugged alpine meadows above 1500m, south-facing (potential pasture sites) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_class=alpine_meadow&min_elevation=1500&aspect=S,SE,SW&sort=area_sqm&sort_dir=desc' # Dense urban building blocks (auto_subclass=dense, ≥3 buildings, all tall) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?auto_subclass=dense&min_buildings=3&min_building_stories=4&sort=building_max_height_m&sort_dir=desc' ``` ### Auto-class decision logic (parcel_compact.classify_parcel) Priority-ordered ladder. First match wins. 1. water_body water frac ≥ 0.40 2. disturbance recent Hansen ≥ 20% area OR tree_loss/excavation/fill/construction ≥ 25% by frav 3. built_up ≥ 35% built footprint OR ≥ 5 buildings subclasses: apartments (≥5 stories), multi_storey (≥3), house (≥2), dense (≥60% built), — 4. infrastructure ≥ 35% road+path+parking+bridge 5. farmstead 5–35% built + ≥1 building + ≥30% farmland/grass subclass with_house when buildings have stories 6. vineyard / orchard ≥5% of frav (low threshold — specialty cultivation) 7. forest ≥ 60% trees; subclass recently_thinned (recent Hansen ≥5%), tall_forest (mean tree h ≥ 25m), regenerating (<8m) 8. wooded 30–60% trees (open_woodland subclass) 9. shrubland ≥ 40% shrub, <30% tree 10. cropland ≥ 50% crop+bare; fallow when bare > crop 11. meadow ≥ 60% grass; subclasses pasture/rugged/orchard_meadow 12. alpine_meadow meadow + slope ≥20° OR elev ≥1500m OR alpine terrain class 13. bare ≥ 50% bare_soil/rock 14. mixed fallback; subclass = strongest sub-signal name `auto_class_confidence` reflects how decisively a parcel falls inside its class (1.0 unambiguous; 0.4 narrowly past threshold; 0.1 = mixed fallback). The classifier consumes (defensively): `frav` (preferred), `area_summary`, `top_trees[*].canopy_height_m`, `slope_mean_deg`, `tri_mean`, `elevation_m`, `elevation_range_m`, `terrain_class`, `hansen_loss.recent_5yr_pixels`, `building_count`, `building_max_height_m`, `building_max_stories`, `building_total_footprint_sqm` (the last four spatially attributed at index build). ### GET|POST /api/v1/parcels/batch ← BATCH PARCEL ENRICHMENT Accepts GET (query params) or POST (JSON body). Three modes: **Mode 1 — Explicit parcel IDs:** ```json {"parcel_ids": ["63349-505/3", "75414-1314/1"]} ``` Max 200 parcels. Returns cadastre + landscape data + conservation score per parcel. **Mode 2 — Cadastre query (supports ALL cadastre query filters):** ```json { "query": { "kg": "63349", "landuse": "W", "min_area": 1000, "max_area": 50000, "has_buildings": "false", "has_legal_refs": "true", "legal_context": "nature_protection", "state": "Steiermark", "district": "Voitsberg", "gemeinde": "Köflach", "plz": "8580", "status": "G", "sort": "area_desc" }, "landscape_filters": { "min_vegetated_fraction": 0.5, "min_ndvi": 0.3, "min_tree_canopy_sqm": 200, "min_elevation": 500, "max_elevation": 2000, "min_conservation_score": 40, "dominant_type": "tree", "sort": "conservation_score", "sort_dir": "desc" }, "limit": 50, "offset": 0 } ``` Query mode first queries cadastre, then enriches with landscape data and applies landscape post-filters. Supports all cadastre /query params (q, kg, gemeinde, district, state, plz, landuse, min_area, max_area, has_buildings, status, ez, has_legal_refs, legal_context, bbox coords, sort). Response: {results: [{parcel_id, kg_code, cadastre: {...}, landscape: {...}, conservation_score: 0-100}], total, offset, limit, meta} **Mode 3 — Landscape-first (compound → parcels) ← THE POWER QUERY:** Starts from our landscape index (compound query on KGs), finds matching KGs, loads per-parcel detail from our KG JSONs, applies per-parcel filters, optionally enriches with cadastre. This answers: "give me 100 parcels where tree confidence > 0.8, area > 800m², grass confidence > 0.8, no buildings, SW aspect, rugged terrain." ```json { "compound": { "type_filters": [ {"type": "tree", "min_confidence": 0.8, "min_area_sqm": 800}, {"type": "grass", "min_confidence": 0.8, "min_area_sqm": 1300} ], "max_buildings": 0, "aspect": ["S", "SW", "W"], "min_roughness": 2.0 }, "parcel_filters": { "min_vegetated_fraction": 0.5, "types": ["tree", "grass"], "min_type_fraction": 0.1, "cadastre_has_buildings": false, "cadastre_landuse": "W", "sort": "conservation_score" }, "cadastre_enrich": true, "limit": 100 } ``` compound supports all SearchIndex.query_compound() filters: bbox, state, district, gemeinde, aspect, dominant_type, phenology, terrain_class, quality_grade, Terrain: min/max_slope, min_roughness, min/max_elevation, min/max_elevation_min (valley floor), min/max_elevation_max (ridge top), min_elevation_range, min_steepness_max, min/max_tri Area: min/max_total_area, min/max_parcels, min/max_segments Buildings: min/max_buildings, min_new_buildings, min_infrastructure, min/max_building_height, min/max_building_max_height, min/max_building_stories, min/max_building_stories_max, min/max_building_pitched_pct, min/max_building_footprint, min_new_building_footprint, min_new_building_height, min_new_building_stories, min_building_height_coverage Trees: min_tree_count, min_tree_height, min_tree_canopy_sqm, min_tree_volume Vegetation: min/max_ndvi, min/max_vegetated_fraction, min_shannon_diversity NDVI harmonics: min_ndvi_amplitude, min/max_ndvi_harm_mean, min/max_ndvi_phase SAR: min/max_sar_vv, min/max_sar_vh Temporal: min/max_dtm_change, min/max_volume_change, min_changed_segments, min_disturbed_volume, min_temporal_stability Quality: min_confidence, min_rf_confidence, max_diverged_pct, max_rf_diverged_count, min_rf_classified_pct, min_quality_score type_filters, landcover_filters, sort, sort_dir parcel_filters (per-parcel from our KG JSONs): min_vegetated_fraction, max_vegetated_fraction, min_elevation, max_elevation, min_slope, max_slope (slope_mean_deg per parcel), min_tri, max_tri (terrain ruggedness index per parcel), terrain_class: str (level, nearly_level, slightly_rugged, ...), aspect: str or list (N, NE, E, SE, S, SW, W, NW — parcel aspect_dominant), types (list), min_type_fraction, min_ndsm_max, max_ndsm_max, min_parcel_area, max_parcel_area, is_vegetated, min_confidence (float, overall combined RF+rules confidence), min_rf_confidence (float, overall RF-only confidence), type_confidence: per-type classification filters (list of dicts), e.g.: [{"type": "tree", "min_confidence": 0.7}] combined [{"type": "tree", "min_rf_confidence": 0.8}] RF only [{"type": "tree", "min_rf_confidence": 0.8, "min_area_sqm": 800}] [{"type": "grass", "min_rules_count": 5}] rules only [{"type": "tree", "max_diverged_pct": 10}] low divergence Each dict keys: type (required), min_confidence, min_rf_confidence, min_area_sqm, min_fraction, min_rf_count, min_rules_count, max_diverged_pct min_forested_fraction, max_forested_fraction (0–1, tree+shrub fraction), dominant_type (str, required type to dominate parcel), min_hansen_recent_5yr, max_hansen_recent_5yr (int, Hansen 2020–2024 loss pixels), min_hansen_total, max_hansen_total (int, Hansen all-time loss pixels), cadastre_has_buildings, cadastre_landuse, cadastre_min_area, cadastre_max_area, cadastre_in_natura2000 (bool), cadastre_natura2000_site (sitecode), cadastre_natura2000_type (A|B|C — Birds/Habitats/both), cadastre_natura2000_habitat (moor|floodplain|river|lake|wetland|forest|alpine| meadow|pasture|valley|hill|steppe|orchard|park|cave), sort (conservation_score|vegetated_fraction|forested_fraction|elevation|ndsm_max|parcel_area|hansen_recent|hansen_total), sort_dir (asc|desc) Per-parcel classification data (from our KG JSON classification.by_type.{type}): segments, area_sqm, mean_confidence (combined), rf_mean_confidence (RF only), rf_count (segments classified by RF), rules_count (by rules), diverged_count ### GET /api/v1/parcels/landscape Same as batch query mode but via GET params. Cadastre params: q, kg, gemeinde, district, state, plz, landuse, min_area, max_area, has_buildings, status, ez, has_legal_refs, legal_context, min_lon, min_lat, max_lon, max_lat, sort Landscape params: min_vegetated_fraction, max_vegetated_fraction, min_ndvi, max_ndvi, min_tree_canopy_sqm, min_elevation, max_elevation, min_conservation_score, dominant_type, landscape_sort, landscape_sort_dir Example: /api/v1/parcels/landscape?kg=63330&landuse=W&min_area=50000 Example: /api/v1/parcels/landscape?state=Steiermark&legal_context=nature_protection&min_conservation_score=40 GET syntax for Mode 3 (compound → parcels): Compound filters: flat query params (state=, min_slope=, type_filter=, etc.) Parcel filters: pf_ prefix (pf_aspect=, pf_terrain_class=, pf_min_vegetated_fraction=, etc.) pf_types=tree,grass pf_aspect=E,SE (comma-separated) pf_dominant_type=tree pf_min_forested_fraction=0.5 pf_min_hansen_recent_5yr=0 pf_max_hansen_recent_5yr=5 pf_min_hansen_total=0 pf_max_hansen_total=50 pf_type_confidence=tree:0.7:500 (repeatable, type:min_confidence:min_area_sqm) pf_is_vegetated=true pf_cadastre_has_buildings=false pf_sort=conservation_score pf_sort_dir=desc pf_cadastre_in_natura2000=true|false parcel centroid in any N2K site pf_cadastre_natura2000_site=AT1205A00 parcel in this specific N2K site pf_cadastre_natura2000_type=A|B|C directive type: A=Birds/SPA, B=Habitats/SCI-SAC, C=both (C also matches A-only & B-only sites) pf_cadastre_natura2000_habitat= habitat tag inferred from sitename: moor|floodplain|river|lake|wetland| forest|alpine|meadow|pasture|valley| hill|steppe|orchard|park|cave cadastre_enrich=true|false (default true; needed for cadastre_* pf filters) Per-parcel terrain query examples: # All east-facing level parcels in Vorarlberg curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?state=Vorarlberg&pf_aspect=E&pf_terrain_class=level&limit=100' # → 125 parcels with aspect_dominant=E and terrain_class=level # High-confidence tree parcels with no buildings, sorted by conservation score curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?min_tree_count=50&pf_types=tree&pf_min_vegetated_fraction=0.5&pf_cadastre_has_buildings=false&pf_sort=conservation_score&limit=50' # Explicit parcel IDs via GET curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?parcel_ids=63349-505/3,75414-1314/1' ### GET /api/v1/query/nature ← NATURE CONSERVATION FINDER Cross-references cadastre parcels, WDPA protected areas, RIS legal refs, and landscape analysis. Returns parcels ranked by conservation value score (0-100). Conservation score breakdown: Legal protection (in any Austrian law): +30 Within WDPA protected area: +20 (near: +10) Vegetation fraction: 0-20 (linear) NDVI quality: 0-15 (linear, clamped 0-0.8) Tree canopy area: 0-15 (log scale) Params: bbox=w,s,e,n Spatial filter state, district, gemeinde Admin filter protected_area= Filter to WDPA area (e.g. "Kalkalpen", "Donau-Auen") legal_context= national_park|nature_protection|landscape_protection| water_protection|species_protection|nature_park|etc. min_vegetated_fraction 0-1 min_ndvi float min_tree_canopy_sqm float min_area_sqm, max_area_sqm parcel area filter (sqm) landuse cadastre landuse (W=Wald, LN=Landwirtschaft) has_buildings true|false sort conservation_score|area|ndvi|tree_canopy|vegetated_fraction limit, offset Example: /api/v1/query/nature?protected_area=Kalkalpen&limit=10 Example: /api/v1/query/nature?state=Steiermark&legal_context=nature_protection&min_area_sqm=10000 Example: /api/v1/query/nature?gemeinde=61630&landuse=W&has_buildings=false ### Natura 2000 bridge (via cadastre-process-api) Cadastre-process-api carries 353 AT Natura 2000 sites and a per-parcel cache (~896k parcels). All filters are pass-through from /api/v1/parcels/batch Mode 2 ("query":{...}) and /api/v1/query/nature — no SRTM-lidar code change needed, the `query` dict is forwarded verbatim. Per-parcel cadastre rows returned by Mode 2 carry `in_natura2000` (bool) and `natura2000_sites[]` ({sitecode, sitename, sitetype A=Birds/B=Habitats/C=both, area_ha}). New filters on Mode 2 / /api/v1/query/nature `query`: has_natura2000=true|false parcel centroid in any N2K site natura2000_site= restrict to one site, e.g. AT1205A00 (Wachau) natura2000_type=A|B|C directive type: A=Birds/SPA, B=Habitats/SCI-SAC, C=both. Indexed (natura2000_parcel_index). natura2000_habitat= habitat class inferred from sitename (natura2000_parcel_habitats table). Tags: moor|floodplain|river|lake|wetland|forest| alpine|meadow|pasture|valley|hill|steppe| orchard|park|cave. Direct cadastre passthroughs (just curl, no bridge wrapper yet): /api/v1/cadastre/proxy → /api/v1/natura2000/{stats,search,site/{code}, site_parcels/{code},point?lon=&lat=,parcel/{kg}/{gnr},kg/{kg}} Cross-API examples (landscape × Natura 2000) — **prefer OUR land cover (`type_filters` / `parcel_filters.types` / `min_tree_canopy_sqm`) over cadastre `landuse=W`**; the cadastre code is the legal designation, ours is the observed cover from BEV LiDAR + Sentinel-2 + ortho RGB-I: ```bash # N2K parcels we have ALREADY PROCESSED, per state. Mode 3 only scans KG # JSONs that exist on disk — implicit "is processed" constraint. Per-parcel # N2K membership comes from cadastre enrichment, filtered via # pf_cadastre_in_natura2000. Mode 3 requires at least one compound filter # (here state=) so it doesn't sweep all 8440 processed KGs. curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?state=Nieder%C3%B6sterreich&pf_cadastre_in_natura2000=true&kg_limit=500&limit=200' # Same, but ACTUALLY FORESTED according to OUR observed land cover # (RF tree confidence ≥0.8, parcel tree fraction ≥0.5, ndsm_max ≥6 m — # real trees, not just legal Wald), no buildings, in Tirol. curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch' -H 'Content-Type: application/json' -d '{ "compound": {"state":"Tirol", "type_filters":[{"type":"tree","min_confidence":0.8,"min_area_sqm":800}]}, "parcel_filters": {"types":["tree"], "min_type_fraction":0.5, "min_ndsm_max":6, "min_ndvi":0.5, "cadastre_has_buildings":false, "cadastre_in_natura2000":true, "sort":"conservation_score"}, "cadastre_enrich": true, "limit": 50}' # Same, restricted to a single Habitats Directive site (Wachau AT1205A00): curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch' -H 'Content-Type: application/json' -d '{ "compound": {"type_filters":[{"type":"tree","min_confidence":0.8,"min_area_sqm":800}]}, "parcel_filters": {"types":["tree"], "min_type_fraction":0.5, "cadastre_natura2000_site":"AT1205A00", "sort":"conservation_score"}, "cadastre_enrich": true, "limit": 50}' # All parcels in one Habitats Directive site (Wachau) enriched with our landscape index curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?natura2000_site=AT1205A00&limit=200' # All Birds Directive (SPA, type=A) parcels in Burgenland, observed-grass curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch' -H 'Content-Type: application/json' -d '{ "compound": {"state":"Burgenland", "type_filters":[{"type":"grass","min_confidence":0.7,"min_area_sqm":500}]}, "parcel_filters": {"types":["grass"], "min_type_fraction":0.4, "cadastre_natura2000_type":"A", "sort":"conservation_score"}, "cadastre_enrich": true, "limit": 50}' # Habitat-class index: ALL MOOR parcels (peatlands) we have processed, # observed wet (low NDSM, vegetated). Backed by natura2000_parcel_habitats # in cadastre-process-api — millisecond-fast indexed join. curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch' -H 'Content-Type: application/json' -d '{ "compound": {"min_vegetated_fraction":0.5}, "parcel_filters": {"cadastre_natura2000_habitat":"moor", "max_ndsm_max":2, "sort":"conservation_score"}, "cadastre_enrich": true, "limit": 100}' # Floodplain forests (Auwald): N2K habitat=floodplain × OUR observed trees curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch' -H 'Content-Type: application/json' -d '{ "compound": {"type_filters":[{"type":"tree","min_confidence":0.8,"min_area_sqm":800}]}, "parcel_filters": {"types":["tree"], "min_type_fraction":0.5, "min_ndsm_max":8, "cadastre_natura2000_habitat":"floodplain", "sort":"conservation_score"}, "cadastre_enrich": true, "limit": 100}' # Cadastre-only habitat lookup (bypass landscape index, fast) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?natura2000_habitat=alpine&state=Tirol&limit=200' curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?natura2000_type=B&state=K%C3%A4rnten&landuse=W&limit=200' # Site-type stats: how many type-A vs type-B parcels in our index? curl 'https://cadastre-process-api.exe.xyz/api/v1/natura2000/stats' | jq '.by_type_parcel_count' curl 'https://cadastre-process-api.exe.xyz/api/v1/natura2000/stats' | jq '.by_habitat_parcel_count' # Steep south-facing N2K grassland (alpine meadow candidates) via compound→parcels curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch' -H 'Content-Type: application/json' -d '{ "compound": {"min_slope": 20, "aspect": ["S","SW","SE"], "min_elevation": 1500, "type_filters": [{"type":"grass","min_confidence":0.7,"min_area_sqm":500}]}, "parcel_filters": {"cadastre_has_buildings": false, "sort": "conservation_score"}, "cadastre_enrich": true, "limit": 100, "query": {"has_natura2000": "true"}}' # Cadastre says forest (W) but WE observe no trees — deforestation / # mis-class hunt. Use OUR landscape signal (max_tree_canopy_sqm) as the truth, # cadastre landuse=W only as the prior expectation. curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch' -H 'Content-Type: application/json' -d '{ "query": {"has_natura2000":"true","landuse":"W"}, "landscape_filters": {"max_tree_canopy_sqm":100, "sort":"hansen_recent", "sort_dir":"desc"}, "limit": 50}' # Outside any N2K but legally protected (RIS-named but not in any polygon) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?has_natura2000=false&has_legal_refs=true&legal_context=nature_protection&limit=50' # What % of a Gemeinde is covered by Natura 2000? curl 'https://cadastre-process-api.exe.xyz/api/v1/natura2000/kg/12358' # Point lookup — which sites cover this coordinate? curl 'https://cadastre-process-api.exe.xyz/api/v1/natura2000/point?lon=15.45&lat=48.36' ``` Note: when `has_natura2000` or `natura2000_site` is set, cadastre defaults to `with_stats=false` for speed; pass `with_stats=true` if you need totals. ### GET /api/v1/query/habitat_count ← FAST CROSS-API COUNT How many Natura 2000 parcels of a given habitat/type/site lie inside KGs we have already processed? Single cadastre round-trip (indexed N2K join) + in-memory set intersection against `kg.processed=1` — no per-KG enrichment loop, no schema bloat. Designed for instant dashboard / forecasting answers. Params (at least one of habitat/sitetype/sitecode required): habitat=moor|floodplain|river|lake|wetland|forest|alpine|meadow| pasture|valley|hill|steppe|orchard|park|cave sitetype=A|B|C Birds / Habitats / both (C also matches A or B) sitecode=AT1205A00 specific Natura 2000 sitecode state, district, gemeinde, landuse, has_buildings optional cadastre filters breakdown=true include by_kg{} and by_state{} maps sample=N also return up to N matching parcel_ids (max 5000) Response: cadastre_total all matching parcels across AT processed_total … of those, in a KG we have processed processed_fraction processed_total / cadastre_total unique_kgs_total / unique_kgs_processed unprocessed_total cadastre_total - processed_total (work remaining) query_time_ms by_kg / by_state only when breakdown=true parcel_ids only when sample>0 truncated true if cadastre had >25 pages of results (unlikely) Auto-paginates cadastre (100k/page) so big habitats (forest ≈ 214k) return complete; small ones (moor ≈ 30k) finish in ~1s. Example: /api/v1/query/habitat_count?habitat=moor /api/v1/query/habitat_count?habitat=alpine&state=Tirol&breakdown=true /api/v1/query/habitat_count?sitetype=A&breakdown=true /api/v1/query/habitat_count?sitecode=AT1205A00&sample=100 /api/v1/query/habitat_count?habitat=floodplain&landuse=W # forested wetlands ### GET /api/v1/parcel//detail Full combined detail for one parcel from both APIs. Includes: cadastre data, legal refs, protected area containment check, landscape analysis (elevation, NDVI, vegetation, classification), conservation score. Example: /api/v1/parcel/63330-913/detail ### GET /api/v1/kg//profile Combined KG profile merging cadastre + landscape. Includes: cadastre info, landuse distribution, legal refs, landscape metrics. Example: /api/v1/kg/63330/profile ### Cadastre Proxy Endpoints These proxy useful cadastre endpoints through our API: GET /api/v1/cadastre/legal/search Params: q, context, type (listed|boundary_walk), bundesland, kg, limit, offset Example: /api/v1/cadastre/legal/search?context=national_park GET /api/v1/cadastre/protected_areas Params: q, near_lon+near_lat, contains_lon+contains_lat, limit Example: /api/v1/cadastre/protected_areas?q=Nationalpark GET /api/v1/cadastre/landuse/distribution Params: kg, gemeinde, district, state, code, abbr, group_by, limit Example: /api/v1/cadastre/landuse/distribution?kg=63330 GET /api/v1/cadastre/landuse/codes Returns all Austrian cadastre landuse type codes. Example: /api/v1/cadastre/landuse/codes ### Cross-API Query Examples ```bash # Find national park parcels with landscape enrichment curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/query/nature?protected_area=Kalkalpen&limit=5' # Forest parcels in Voitsberg with no buildings, landscape-enriched curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/landscape?district=Voitsberg&landuse=W&has_buildings=false&min_area=10000' # Compound → parcels: tree-rich KGs, east-facing level parcels curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?min_tree_count=50&pf_aspect=E&pf_terrain_class=level&limit=20' # Full parcel detail (cadastre + landscape + legal + protected areas) curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/parcel/49407-1531/3/detail' # Combined KG profile curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/kg/63330/profile' # Diacritics-insensitive lookup curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/lookup?q=Premstatten&type=kg' ```