# umfeld-at — context layers around a place in Austria (public API) — stage: alpha base: https://umfeld-at.exe.xyz docs: /api/v1/docs/llm.txt (this file) licence: /api/v1/license (per source) stage alpha: field names and limits may still move between epochs — pin to what you read, send breakage to /api/v1/feedback. JSON, CORS *, gzip, ETag + Cache-Control. No key, no login. Please keep ≤ 5 req/s sustained per client. ## What this is This API answers "what is AROUND this point?" for any WGS84 coordinate in Austria: · municipality (Statistik Austria Gemeinde, Bezirk, Bundesland) · land price estimate (Statistik Austria Baugrundstücks-/Boden-/Pachtpreise, modelled) · Natura 2000 sites (EEA) and national parks / protected areas (WDPA) · OpenStreetMap proximity: nearest road, major road, rail, transit stop, water, settlement, remoteness score; raw OSM geometries for a bbox · official geographic names (BEV DLM-Namen: Almen, farmsteads, Rieden, peaks, waters) · legal references from RIS (Landesrecht) per Katastralgemeinde code · register lookups: Gemeinde / PLZ / Ortschaft / KG names and codes The cadastre itself stays client-side: parcels, footprints and land use come straight from the official BEV vector tile cadastre (VTC, CC BY 4.0). vtcseamless (https://github.com/raffopenssh/vtcseamless, open source, MIT; Go module + CLI + bevdirect-serve) stitches the tiles into seamless parcel geometries in your client; Python: https://github.com/raffopenssh/vtcseamless-py (bevdirect-serve client + ne_cells + this API). You then send us a point plus the parcel features you already hold (see /api/v1/context). ## Keys and conventions lon, lat WGS84 decimal degrees (lon 9–17.5, lat 46–49.1). lon first. gemeinde_code 5-digit Statistik Austria Gemeindekennziffer, string ("61630") district_code first 3 digits of gemeinde_code ("616") · state code = first digit kg_code 5-digit zero-padded Katastralgemeinde code ("01503"). Only used as a key for legal refs and toponyms; we never resolve a point to a KG. Every response: meta.license {license, license_url, attribution, dataset, modified} + headers Link: <…>; rel="license" and X-Data-Attribution. They differ per endpoint — store and show them with the data. Pagination: limit / offset where listed; meta.total. ?pretty=1 indents JSON. Errors: {"error": "..."} with 4xx/5xx; 503 + Retry-After means a dataset is still loading after a restart — retry, don't cache. ## The one call: GET /api/v1/context GET /api/v1/context?lon=15.0857&lat=47.0665 GET /api/v1/context?lon=15.0857&lat=47.0665&area_sqm=812&building_count=1 &footprint_area_sqm=140&landuse_areas=41:140,52:672&kg=63349 GET /api/v1/context?lon=…&lat=…&include=municipality,natura2000&radius_m=800 Everything below bundled for one point, one round trip. Blocks (include=, default all; legal only when kg= is given): municipality {gemeinde_code, name, district_code, district_name, state} land_price Statistik-Austria-calibrated estimate — needs area_sqm; honours building_count, footprint_count, footprint_area_sqm, building_period (pre1960|y1960_1990|post1991), compactness (0–1), landuse_areas=NS:m²,… (your Benützungsart split) or landuse_codes=51,52, typology (urban|suburban|rural_settled|rural_agrarian), year natura2000 {in_natura2000, sites[{sitecode, sitename, sitetype, site_type_label, area_ha, habitats[]}]} protected_area {in_protected_area, sites[{name, desig, desig_eng, desig_type, iucn_cat, rep_area}]} osm the osm{} metrics block (see OSM below); area_sqm refines road_on_parcel toponyms {radius_m, total, rows[]} names within radius_m (default 500, max 2000), filters layer=, f_code= as in /toponyms/near; limit (default 20, max 50) legal RIS refs for kg_code (filters context=, type=, law=) + details link data.sources[] which datasets answered; data.missing{block: reason} for blocks that could not be served (dataset not loaded, outside Austria, area_sqm absent). A block is never a reason for a failed request. Typical latency 5–40 ms. ## Municipalities & register lookups (Statistik Austria, EDM) GET /api/v1/search/municipalities?contains_lon=&contains_lat= Gemeinde containing a point GET /api/v1/search/municipalities?q=Gerasdorf | ?id=61630 | ?district=617 | ?state=Steiermark | ?list=all&limit=5000 rows: gemeinde_code, name, district_code, district_name, state, lon, lat (centre) format=geojson → official Statistik Austria polygons GET /api/v1/search/gadm?q= | ?contains_lon=&contains_lat= GADM v4.1 (legacy, non-commercial licence) GET /api/v1/lookup?q=Kofla | ?q=8580 | ?q=Wien&type=gemeinde | ?q=61630&type=kg | ?q=Pflegalm&type=toponym Diacritics-insensitive autocomplete across 2 092 Gemeinden, 7 850 KG names/codes, 16 988 Ortschaften, 2 236 PLZ (federal EDM register). Shorthand: ?kg= ?plz= ?gemeinde= ?ortschaft=. rows: {type, code, name, plz[], gemeinde_code, gemeinde_name, kg_code, kg_number}. type=kg returns the KG's code and name only — it is a register row, not cadastre. GET /api/v1/search/address_osm?q=Innsbruck+Hauptplatz&limit=3 Nominatim geocoding (OSM, 1 req/s) ## KG universe — GET /api/v1/kgs (the contract: kg_count = 7850) GET /api/v1/kgs ALL Katastralgemeinden in ONE gzipped list (~520 KB gz / 1.8 MB). → data{kg_count: 7850, kg_count_contract: 7850, universe_hash, universe_hash_recipe, generated_at, peer_check{service: "srtm-lidar-at", agree, checked_at, …}, kgs[{kg_code, kg_name, gemeinde_code, gemeinde_name, district_code, district_name, state_code, state_name, bbox[min_lon,min_lat,max_lon,max_lat], centroid[lon,lat]}]} universe_hash = sha256(join(sorted(kg_code), "\n")) hex — the fingerprint of the SET of codes; it is also the ETag of this response and the header X-KG-Universe-Hash. Recompute it from your own list: equal hash ⇒ same universe. srtm-lidar-at serves the same 7 850 (/api/v1/kgs?processed_only=0 there); peer_check (re-verified hourly) and the header X-KG-Universe-Peer-Agree say whether the two agree right now. The ETag only changes when the SET of codes changes or the peers disagree — never because checked_at moved. Fetch once, revalidate with If-None-Match (304), Cache-Control max-age 3600. kg_count ≠ 7850 or a changed universe_hash means the universe changed — treat as a new epoch. GET /api/v1/kgs?fields=codes {codes:[…]} only (~60 KB) — enough to compute the hash GET /api/v1/kgs?codes=63349,01004 subset (request order kept, unknown codes dropped → count) unpadded "1004" is accepted and answered as "01004"; kg_code in the list is ALWAYS 5 digits — pad before hashing (universe_hash) GET /api/v1/kgs?district=Voitsberg | ?district_code=616 | ?gemeinde_code=61631 | ?state=Steiermark | ?state=6 district= is an EXACT, case-insensitive Bezirk name (or the 3-digit code). Bezirk "Waidhofen an der Thaya" exists twice (322 + 325); district_code picks one. This replaces the old states → districts → /search/kg loop; register keys only, no parcel data. ## Land prices (Statistik Austria, modelled) GET /api/v1/land_prices/point?lon=&lat=&area_sqm=[&building_count=&footprint_area_sqm=&landuse_areas=&compactness=&year=] Gemeinde resolved from the point, then the same model as /predict. Returns {municipality, estimate{class, buy_eur_per_sqm, buy_total_eur, rent_eur_per_ha_year, rent_eur_per_year, baseline_eur_per_sqm, baseline_source, baseline_year, factors{size, shape, typology}, confidence, building{…} when footprint_area_sqm given}}. GET /api/v1/land_prices/predict?gemeinde_code=61630&area_sqm=750&building_count=1&compactness=0.6 Same model, Gemeinde given explicitly. Pure function of its parameters. GET /api/v1/land_prices/gemeinde/61630 · /bezirk/616 · /bundesland/6 (or /bundesland/Steiermark, /AT) The published Statistik Austria series for that area: statistik_austria{value, year} for the requested ?year= (default 2024; interpolated/extrapolated with the fitted CAGR when the year is not observed), series{…: {year: value}} with every year we hold, bauland_cagr / trends_cagr. Gemeinde: Baugrundstückspreise. Bezirk: Bauland, Häuser, Wohnungen (+ by Bauperiode). Bundesland: Acker-/Grünland buy + rent. Model: class from your features (built → bauland_built; NS 40–47/91 unbuilt → bauland_zoned; 51/62 → ackerland; 50/52–55/58/61 → gruenland; 56/57 → wald), then price = SA baseline(year) × calibration × f_size × f_shape × f_typology. These are model outputs, not valuations; 89 % of Gemeinden within ±25 % of SA for 2024. ## Protected areas GET /api/v1/natura2000/point?lon=&lat= EEA Natura 2000 sites containing the point GET /api/v1/natura2000/site/AT1205A00[?geometry=1] one site (+ MultiPolygon) GET /api/v1/natura2000/search?q=Wachau | ?type=A|B|C | ?list=all&limit=400 A = Birds Directive (SPA), B = Habitats Directive, C = both. habitats[] are tags inferred from the site name (moor, floodplain, river, lake, forest, alpine, …). GET /api/v1/search/protected_area?contains_lon=&contains_lat= | ?q=Nationalpark | ?near_lon=&near_lat= 43 WDPA areas (national parks etc.); format=geojson for polygons. WDPA terms: non-commercial. ## OpenStreetMap proximity (ODbL) GET /api/v1/osm/point?lon=&lat=[&area_sqm=] osm{}: dist_road_m + road_fclass/name, road_on_parcel (needs area_sqm), dist_major_road_m + major_road_fclass/name/ref, dist_rail_m, dist_transit_m + transit_fclass/name, dist_train_station_m + train_station_name, dist_water_m + water_fclass/name, dist_settlement_m + settlement_fclass/name, remoteness 0..100 (0 central, 100 remote; weights road/5 km .30, major road/20 km .15, transit/10 km .30, settlement/15 km .25). GET /api/v1/osm/geometry?bbox=west,south,east,north[&cat=road,rail,water,transit,place][&major=1][&limit=] GeoJSON FeatureCollection of raw OSM geometries to draw context fast. Line props: cat, fclass, name, ref, major; point props: cat, fclass, name, train_station, population. Lines are ≤24-vertex chunks sharing endpoints. Public tier: bbox only. GET /api/v1/osm/stats import date, counts Attribution: "© OpenStreetMap contributors", ODbL. A database you redistribute that merges osm{} fields becomes share-alike — keep OSM-derived columns separable. ## Geographic names — BEV DLM-Namen (CC BY 4.0) 233k official names in seven layers: siedlung (settlements down to single farms and Alm huts), gebiet, gelaende (peaks, passes, valleys), gletscher, gewaesser, sonstige (huts, chapels, castles, ruins, caves), ried (104k Riednamen — field names). Points, positional accuracy ±50 m. Every row: id (BEV GLOBALID, stable), layer, f_code, f_name, name, variant/minority-language names, elevation_m?, kg_code, kg_name, gemeinde_code, gemeinde_name, district_*, state_*, lon, lat, stichtag, currency_year. GET /api/v1/toponyms/near?lon=&lat=[&radius=500][&layer=siedlung,ried][&f_code=] what is this place called GET /api/v1/toponyms/search?q=Lignitzalpe[&layer=&kg=&gemeinde=&district=&state=&near=lon,lat&radius=&bbox=&min_score=] Fuzzy (trigram, stemmed: "Liegnitzalm" finds "Lignitzalpe"), score 1.0 exact … 0.55. Names repeat nationally ("Hochalm" = 1 099 rows) — constrain by area or near=. GET /api/v1/toponyms/{id} full row (public tier: without the parcel block) GET /api/v1/kg/{kg_code}/toponyms[?layer=ried] everything named inside one KG, Rieden first GET /api/v1/toponyms/stats stichtag, counts per layer format=geojson|csv on search/near. Attribution: "Datenquelle: BEV – Geographische Namen (DLM), CC BY 4.0, bearbeitet" with the Stichtag from /toponyms/stats. ## Legal references — RIS Landesrecht GET /api/v1/legal/kg/{kg_code}[?context=national_park&type=listed|boundary_walk&law=Kalkalpen] Laws (Verordnungen) that name parcels of this KG: refs[{law, legal_context, parcel_list_type, grundstueck, ris_url, …}], total_refs, unique_laws, legal_contexts. The Grundstücksnummern here are quoted from the law text, not from a cadastre. Filter client-side by your gnr. GET /api/v1/legal/search?q=Kalkalpen | ?context=water_protection&bundesland=Oberösterreich | ?type=boundary_walk&kg=50207 GET /api/v1/legal/stats contexts: national_park, nature_protection, landscape_protection, nature_park, water_protection, species_protection, admin_boundary, zoning, monument_protection, viticulture, hunting, forestry, other. ## NE cells — declared land-use statistics per H3 cell (tokenless, H3-keyed) Complementary to srtm-lidar-at (https://srtm-lidar-at.exe.xyz:8100/ — the OBSERVED layer per cell: LiDAR heights, tree apices, structures, NDVI, forest loss, terrain; same H3 res-12 ids; GET /api/v1/cells?bbox= | ?lon=&lat=&k= | /api/v1/cells/{h3} | /api/v1/kg/{code}/cells, spec /api/v1/docs/llm.txt#cells there). We serve the DECLARED layer (lu = BEV survey, bev = bevdirect baseline) and the obs rows srtm posts to us opaque. Join = cell_id, client-side. Contract: /api/v1/docs/ne.md · attribution "© BEV (CC BY 4.0), bearbeitet · observed layer © srtm-lidar-at (CC BY 4.0)". Rate: 20 r/s per client (burst 60) → 429. GET /api/v1/ne frozen{} pin, endpoints, counts, dictionary ids GET /api/v1/ne/manifest[?fields=digests] per-KG status/digests (ETag; digests ≈ 60 KB gz) GET /api/v1/ne/manifest?since= delta: only kgs with updated_at ≥ since + counters, delta:true; cursor = generated_at of your last answer; older than retention_s (7 d) → full body, delta:false (replace) GET /api/v1/ne/{kg}[?layers=lu,bev,obs][&format=json] whole-KG container (application/vnd.ne-cells, ETag "ne--", Range, 304; unbuilt → 404 + Retry-After) GET /api/v1/ne/{kg}/head section headers only GET /api/v1/ne/dict/{id} zstd dictionary (16 KB, immutable) named by X-NE-Dict GET /api/v1/ne/chunk/{h3_res9} THE primitive: all rows of one res-9 cell (343 res-12 children) format=zstd (default) stored frames pass-through, X-NE-Frames: kg:layer:off:len:rows:row_bytes, X-NE-Dict format=bin rows deduped across KGs, X-NE-Rows: lu=30:n;k=29:n[;obs=rb:n] format=json columnar X-NE-KGs: 63330:0.80,63333:0.20 (share of the chunk per KG) · ETag immutable per digest tuple GET /api/v1/ne/cell/{h3} res 12 → {cell, kgs:{kg:share}, lu:{…}, k:{…}|null, obs:{…}|null} res 10 → K record + children[]; res 9 → 302 to /chunk GET /api/v1/ne/cells?ids=a,b,… ≤ 512 ids (res 9/10/12 mixed), format=bin (default) | json GET /api/v1/ne/cells?lon=&lat=[&k=0..3] res-12 cell of the point + k rings GET /api/v1/ne/cells?bbox=w,s,e,n[&list=1] res-12 cells whose CENTRE is in the box (span ≤ 0.05°, ≤ 16 384 cells else 413 {split:[4 sub-boxes]}); list=1 → {chunks:[res-9…], etags:[…]} POST /api/v1/ne/cells {"ids":[…]} ≤ 16 384 ids GET /api/v1/context?lon=&lat=&include=ne block ne = /ne/cell of the point (missing.ne when unbuilt) GET /api/v1/ne/stats[?kg=A,B|gemeinde=|district=|state=] CHANGE over time: change{since, confirmed{cells, d_lu_m2[9], d_str_cover_m2, d_str_n, transitions_m2{"green>structure":…}}, candidate{…}, by_day{}}; no filter → national. m², additive; confirmed = the new cell statistics were sighted twice (next pass or a second observer); candidate = once. layers=chg (json only, /chunk and /cells) → chg{n, cells[]: {status, since, last_change, d_lu_m2[9] m², d_str_cover_m2, d_str_n, d_edge_m_ha, from, to}|null per id}; /cell/{h3} always carries chg. Change = difference of two frozen rows of the same cell between OUR observation dates (never a register date); register-derived fields (gk, n_units, K rows) are outside the change series by construction. fields=lu,str_cover,… projects json; layers=lu[,obs][,bev] (obs public only once released: obs_public in /ne) Partial answers (an unbuilt KG touches the area): pending_kgs[], X-NE-Pending-KGs, Cache-Control: no-store. pending ≠ "no cells". Records (little-endian; identical bytes on every tier; shares are 1/255 of CELL area) LU row 30 B, res 12 (~307 m²): cell_id u64 | kg_share u8 | land u8 | cov u8 | flags u8 (bit0 declared_unknown, bit1 normalised) | lu u8[9] in group order geb bau acker gruen wald wasser verkehr alpen sonst | str_cover u8 | str_n u16 | gk u8 (surveyed-boundary share) | n_units u8 | edge_m_ha u16 | orient_coh u8 (0xFF none) | orient_axis u8 (axis/2, 0xFF none) K row 29 B, res 10 (~1.5 ha), only where n_units ≥ 5: cell_id u64 | n_units u16 | unit_p10 u32 | unit_p50 u32 | unit_p90 u32 | reg_n u16 | units_per_reg×10 u8 (0xFF none) | str_n u16 | str_p50 u16 (0xFFFF none) Public tier: unit_p10/50/90 are class midpoints of 1/8-octave log buckets (≈ 9 % steps); p10/p90 = 0 (json null) where n_units < 10. OBS row: peer-defined, record_bytes + fields[] in the section header; first 8 B = cell_id. Border cells appear in every KG they touch (bytes equal except kg_share) — dedupe by cell_id, take the largest share. Decode (JS): const v = new DataView(buf); for (let o = 0; o < buf.byteLength; o += 30) { const id = v.getBigUint64(o, true).toString(16), land = v.getUint8(o+9), lu = new Uint8Array(buf, o+12, 9), strN = v.getUint16(o+22, true), edge = v.getUint16(o+26, true); } Decode (py): struct.iter_unpack(", the reference implementation — never a port), report digests with a contributor token to POST /contrib/api/v1/ne/{kg}/report; the answer lists chunks that changed against the baseline. Tokens on request via POST /api/v1/feedback. Protocol: /api/v1/docs/ne.md § Observers / § Epochs. Ready-made: pip install git+https://github.com/raffopenssh/vtcseamless-py → "vtcseamless observe --kg --observer