# 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 · NE cells: declared land use per H3 cell and LAND-USE CHANGE over time (section "NE cells" below — API, plus the landing page GET / whose every piece a client can rebuild: see "Landing page") 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. GET /api/v1/ne/glyph landing-page glyph data: one row per H3 res-7 cell of Austria, fields[] = [x, y, chunks, passes_sum, reports, age_h, observed, change_cells, kg] ([x, y] only when not digested yet); kgs[] names, kg_codes[]; w/h = viewBox; 5 min cache. GET /api/v1/ne/glyph/kgs the glyph's search index: every KG of the universe as [code, name, gemeinde, district, x0, y0, x1, y1] (bbox in viewBox units); 1 h cache. GET /api/v1/ne/glyph/tile?lod=0|1|2&i=&j=[&v=obs|tree|chg|sub|subc] full-resolution PNG tile (lon/lat grid 0.025° / 0.01° / 0.1°): one hexagon per res-12 cell, dominant group (alpha = dominance), unknown use = neutral; v=obs → the OBSERVED land use per cell (srtm-lidar-at's obs_lu, same palette and dominance rule; cells without an observed row stay transparent), v=tree → canopy layer over v=obs: every wooded cell in the observed teal, deeper by the band of its tallest tree (cells without a tree transparent), v=chg → candidate/confirmed change; header X-NE-Tile n=;lu=9 sums (observed sums under v=obs|tree);unk=;obs=cells painted;trees=Σ tree_n;tmax=tallest tree (0.5 m);th=5 bands of cells by tallest tree (<5,5–15,15–25,25–35,35+ m); chg=confirmed,candidate;dlu=9 m². v=sub → INVEKOS subsidy claims (farm-subsidies-austria, AMA Schläge, CC BY 4.0): lod 0/1 every Schlag of the year as drawn (their IVP1 outline bins, 0.5 m simplified), ochre fill ≈ .47 + edge, hatched = dubious hand-drawn outline, GLÖZ landscape element = edge only; v=subc → the claims' lineage vs the previous year filled like the change cells but ochre: new ≈ .64 + bold edge, redrawn (modified/split_merged/partial) ≈ .47 + hairline, gone ≈ .39 hatched + dotted edge, same = nothing. Hexagon fallback when an outline bin is missing (wash by claimed share / lineage as outlines of runs of cells). lod=2 paints the res-7 overview (subc: wash by the share of the claimed area that moved). 204 + X-NE-Tile down=1 while the peer is down. Meta adds sub=cells;cov=Σ share (1/255); cls=8 cov-weighted sums (none acker gruenland alm wein dauerkultur gloez sonst);bio=Σ,unknown; dub=cells with rel<150;chg=same,modified,split_merged,partial,new,gone;dcov=Σ Δshare; cells=res-12 cells in the box;year=;prev=;rel=release[;chg7=% of claimed area moved] [;pn=Schläge of the year anchored (first vertex) in the box;pchg=6 counts by lineage kind; pha=Σ m²;pnew=Σ m² new;pgone=Σ prev m² gone — present iff painted from outlines]. GET /api/v1/ne/glyph/sub the subsidy overview: {year, prev_year, release, n, cov[], cls[], bio[], dcov[], chg[], cls2[], shr[]} in the cells[] order of /ne/glyph (1/255 of the res-7 cell; bio 255 = unknown; chg = share of the claimed area with a lineage event; cls2 second class, shr = dominant class share of the claimed area); 24 h; 503 no-store while the peer is down. Source https://farm-subsidies-austria.exe.xyz/api/v1/invekos/h3?res=7 — the raw per-cell columns are theirs; we paint and sum, keep nothing on disk. GET /api/v1/ne/glyph/cell?lon=&lat=|cell= the res-12 cell under a point: public cell doc + its hexagon in viewBox units + pic_obs = what the v=obs tile shows there, in words ({dom, dom_pct (5 % steps), second?, tree?: {band 0–4, h_m, n}} | {unknown:true} | null = no observed row) — the pixel, not the row; the per-cell observed row stays behind obs_public. &sub=1 adds sub{claimed, claimed_pct, class, bio (true|false|null unknown), reliable, vs_prev (same|modified|split_merged|partial|new|gone), d_claimed_pct, year, prev_year, release, schlag?} from the subsidy layer (sub:null + sub_unavailable:true while the peer is down); schlag = the outline under the cell centre {area_ha, class, bio, reliable, vs_prev, d_area_ha? (vs best-linked predecessor), prev_area_ha? (gone), clipped?}. GET /api/v1/ne/glyph/kg?kg= the KG's outline at res 12 (union of its majority cells, holes kept) as an SVG path d in viewBox units + bbox + cells; 404 until the KG is held; 1 h cache. Landing page GET / — the third way into land-use change, next to /ne/stats (figures) and layers=chg (cells). This is the complete inventory of what it shows and the endpoint behind each piece, so a client (e.g. a map viewer, a settlement explorer) can show ALL of it itself. HTML for humans; never scrape it — use the JSON named. 1 Map glyph (data /ne/glyph, 5 min): one dot per H3 res-7 cell, fields [x,y,chunks,passes_sum,reports,age_h,observed, change_cells,kg]. Meaning: ink depth = passes_sum/chunks (how often peers digested that land, α = alpha·coverage raised to depth); tint = age_h < fresh_hours (6) → fresh report, fading by age; hairline = [x,y] only (never digested). hex{} = hexagon outline per cell for seamless faces when zoomed. Palette, one hue per meaning: plum = coverage depth, gold = fresh, teal = observed, vermilion = change; blue is UI only; CVD-safe. 2 Views (same data, one picture): "declared" (ink depth), "observed" (observed > 0 → teal, same depth ramp as the declared ink; layer srtm-lidar-at posts) and "change" (change_cells > 0 → vermilion, darker = more cells, soft halo at country scale) — a text switcher under the figure (declared · observed · change; the active word takes the layer colour, a dot-loader trails it while its tiles load), key v cycles; the overlay holds at every level of detail (dots, hex faces, full-resolution tiles with &v=obs|chg) and the card reorders its sources so the view's source reads first. 2a Full resolution ("full" button, ;full in the link): the declared product at native res 12 as PNG tiles from /ne/glyph/tile — enabled only once the view is zoomed ≥ 8× (the button never moves the view); LOD by zoom: lod=2 (0.1° tiles) from 8×, lod=0 (0.025°) from 110×, lod=1 (0.01°) from 300×, up to 900×; the res-7 dots/faces step back under it and the pointer picks the res-12 cell via /ne/glyph/cell?lon=&lat=. Under full res the selected KG / pinned res-7 cell shows as a white veil (55 %) over everything else instead of an outline; for a KG the veil follows its exact res-12 outline (/ne/glyph/kg), not the res-7 hexagons. The observed view at full res paints each cell's observed use (same colours as declared) and its legend adds the trees in view: count, tallest, and a bar of cells by tallest-tree band. Relief (view-bar icon after the tree toggle, ;relief in the link): srtm-lidar-at's hillshade XYZ tiles (GET https://srtm-lidar-at.exe.xyz/tiles/hillshade/{z}/{x}/{y}.png, 25 m DTM, CC BY 4.0) blended faintly over the cells under full res, full weight at the coarse tiles and fading out by 300× where the 25 m cells would only show their seams; legend adds the source line. Any view, no data of ours. Subsidy (tractor icon after "declared" and again after "change" — one toggle, ;subsidy in the link, key s): INVEKOS claims per cell from farm-subsidies-austria (AMA Schläge, CC BY 4.0, aggregated there per H3 cell). Under declared: ochre dots / hex faces by claimed share wide out, and in full resolution every Schlag as drawn (hatched = dubious outline, GLÖZ = edge only), the ink steps back; under change: the claims' lineage vs the previous year filled in ochre over the vermilion change fills (full = new · half = redrawn · hatched = gone); under observed: not painted (the icons fade; a click there switches to declared). Legend block (declared): "n % of the land in view claimed (year-release) · bio n % | bio unknown · dubious drawing n % · m Schläge · ha" + class bar; (change): "k of m Schläge moved · Δ claimed ±ha" + new (+ha) / gone (−ha) / redrawn / same counts; last line the attribution. Cards: one SUBSIDY row — res-12 "claimed n % · class · bio · drawn reliably | dubious drawing · vs : same|new|gone|redrawn · Schlag x ha (±Δ)" or "not claimed", res-7 "claimed n % of the cell · class n % · class2 n % · bio n %", KG = sum over its res-7 cells. Hover peek = what the picture shows there: the Schlag "x ha · class · bio · dubious drawing [· vs : kind ±ha]" when one is under the cell, else the cell reading. Peer down → "subsidy layer unavailable". Data: /ne/glyph/sub, tile?v=sub|subc, cell?sub=1. Hover peek under full res (with a KG or cell selected): one line per source for the cell under the cursor — a declared bar + its top groups + structures, under v=obs the observed pixel in words (pic_obs), under v=chg the change state. The KG is not repeated (the selection names it). 3 Cell card (hover/tap/arrow keys; ⋯ expands; collapsed = one line per source, sources ordered by the view): KG · Gemeinde (kgs[] / kg_codes[] index of /ne/glyph), chunks of 343, reports, passes per chunk as a depth bar (pass bands), last report age; a CHANGE section — confirmed cells at once, then async from /ne/stats?kg= the KG's Δ per group, candidates, since, digests and the qc line (pipeline/jitter/coverage excluded, clustered/isolated); every legend lists all of its bar's groups (first few, the rest behind …); and a live OBSERVED section fetched straight from srtm-lidar-at (GET https://srtm-lidar-at.exe.xyz/api/v1/kg/{code}/cells?format=json → summary{cover_ha, cells_observed, structures_n, trees_n, terrain} shown at once, then per-tile cells for the hexagon) — lon/lat of a dot = lon = ((x−pad)/scale + minx)/kx, lat = −((y−pad)/scale + miny) with proj{} of /ne/glyph. Self-check for a client that claims to show land-use change: 1 depth + fresh + undigested, 2 observed and confirmed change as separate views, 3 per-cell card with declared AND observed side by side, 6 find a KG, 6/7 highlight a KG and a cell and share that exact view, 8–10 the daily digest with pass bands, QC and confirmed m². If one is missing, people miss it. 4 Zoom/pan: pinch, double-tap, ctrl+scroll, drag; keys + − 0 Esc; exact cell pick at 24×. Static SVG works without script; prefers-reduced-motion drops the transitions. 5 Tour (teaser): ~14 cells worth a look now — fresh report, confirmed change, deep passes, spread across the country — inlined as data-picks; the cursor visits six, any input stops it. Same scoring is yours from /ne/glyph fields. 6 KG search ("find", key /): name / Gemeinde / district / 5-digit code, diacritic-insensitive, inline completion; index /ne/glyph/kgs (1 h) = [code,name,gemeinde,district,x0,y0,x1,y1]. Pick → zoom to the KG bbox + KG highlight (every cell with kg = code lit, mask outline; white veil under full res); tap → cell highlight (one dot). "recent" = past picks (localStorage). 7 Share link / deep link ("link" button copies the URL of what is on screen; opening it restores the view, also on hashchange — so an agent explores the page by WRITING the hash, no clicks needed: set location.hash or open /#… and every token below is applied live). Grammar, one string, tokens joined by ";": selection (first): #kg=63349 #cell=x,y (pinned at the overview unless ;at=/;z= say otherwise) #h3= #q= (first hit of the KG search — name / Gemeinde / district / code; rewritten to #kg= once resolved) ;v=obs|chg view (declared when absent) ;full full resolution (res-12 tiles) ;nofull no automatic full res at the deepest zoom ;trees every tree as a ring of its crown (srtm apices; observed view — implies ;v=obs when no view is given; drawn from the near tiles, ≥110×) ;relief hillshade from the 25 m terrain model blended under full res (any view; full weight at the coarse tiles, gone by 300×) ;subsidy INVEKOS subsidy claims (any zoom; painted under declared and change, not observed) ;screen full screen ;z= zoom about the selection's centre, no fly — ≥300 reads single cells (read-only, never written back; e.g. /#q=kohlschwarz;v=obs;full;trees;relief;z=400 and the same with ;v= and ;trees dropped is the declared/observed pair of one spot) ;at=x,y,z centre + zoom (written once zoomed → the exact picture) ;open card expanded ;below card docked below the map (floats translucent by default; drag it by its grip or header anywhere — off the map too, double-click the grip snaps it back; the ⤓ button docks it) ;card=fx,fy dragged card, map-box fractions Toggles that cannot show at the current zoom (trees below 110×, relief past 300×, full below 8×) or view (subsidy under observed) stay in the hash and come back with the zoom / view; the bar fades their icons meanwhile. x,y in viewBox units; #cell snaps to the nearest dot, so from lon/lat: x = round((lon·kx − minx)·scale)+pad, y = round((−lat − miny)·scale)+pad. Mint these yourself to hand a person the change picture of a place ("confirmed change in KG 63349" → /#kg=63349;open; "the trees and the ground of that hill" → /#q=;full;trees;relief;z=400). 7a Scripted checks (window.neGlyph, for a browser driver): state() {hash, view, z, vb, full, trees, relief, subsidy, subsidyLoaded, reliefZ, reliefLeft, lod, left, tiles[] {k, v, on, ok, n, m = X-NE-Tile meta}, sel, pinned, pin, loading}; settled(ms) → promise once nothing loads; go('#kg=…;full;trees') → sets the hash and resolves with settled(); card() / legend() / peek() = the card's, the in-view legend's and the hover peek's text; hover(lon,lat), pin(lon,lat), view('obs'|'chg'|''), trees(bool), relief(bool), subsidy(bool), lonlat(x,y), xy(lon,lat), client(lon,lat). Soak: import('/api/v1/ne/glyph/soak.js').then(m=>m.run({spots:2})) on the landing page walks every view/overlay by share link and returns one string (PASS|FAIL + one line per failure) — layers complete, overlays shown iff on, JSON ↔ dots ↔ tiles ↔ cell docs consistent, share links round-trip. A declared-vs-observed check of one spot: go('#q=;full;z=400'), legend(); view('obs'), settled(), legend(); hover a lon/lat and read peek() — then /ne/glyph/cell?lon=&lat= gives lu[] and pic_obs for the same cell. 8 Bar chart "Declared layer digested by peers" (figures /ne/stats change.by_day/qc_by_day + /ne coverage; sources per report /ne/manifest): one bar per day, fixed window, height = km² of res-10 chunks (~1.5 ha) peers re-digested that day, stacked by pass band (1st sight, 2nd, 3rd+ …; hatch fills); flagged days = unusual. 9 Bar tooltip (hover floats above the bar, docks below on touch/narrow; click/Enter pins, ←/→ walk days, Esc releases; text without script): weekday · date · rank chip · DECLARED: km² · KGs · reports · digest shape (whole-KG ≈ 1× KG area vs viewport burst ≪) · pass mix as a mini stacked bar · comparable share · that day's change with QC (confirmed / candidate / pipeline / jitter / coverage) · OBSERVED: rows srtm posted that day (a different measurement, never mixed into the bar) · footer: sources bevdirect@tag ×n · first-sight coverage to date. 10 Caption line under the chart: chunks ever digested · confirmed change since … (m²) · the report endpoint. Nothing on the page is per land unit — all H3-keyed; attribution as above on anything you rebuild. layers=chg (json only, /chunk and /cells) → chg{n, cells[]: {status, since, first_seen, last_seen, 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; /ne/glyph/cell adds cluster{cells, confirmed, candidate, area_m2, d_lu_m2, d_lu_pending_m2, d_str_n, first, last, capped} = the touching res-12 cells with change around it (cap 6000). 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("<QBBBB9sBHBBHBB", rows) K: "<QHIIIHBHH" zstd chunks: dictionary = GET /ne/dict/{X-NE-Dict} once; python: zstd.ZstdDecompressor(dict_data=zstd.ZstdCompressionDict(d)) Playbook (map client): per viewport tile GET /ne/cells?bbox=…&list=1 → fetch the /ne/chunk/{c9} you lack (24 h, keyed by ETag) → colour res-12 hexes from lu[9] / str_cover / edge_m_ha; flags.declared_unknown → fog. Click: /context?lon&lat&include=ne → K record (unit_p50, str_n, orient_axis). Coverage: as of 2026-10-26 22:50 UTC: 7 850 of 7 850 KGs with a declared layer (100.0 %), 5 718 with an observed layer live at srtm-lidar-at (4 more already shared with us, awaiting publication there — poll, not a gap) [peer state 22:37 UTC]; 278 326 942 res-12 cells in 1 040 925 res-9 chunks. Grows by submission (a KG appears the moment its cells land in the store) — GET /ne → kgs_built; GET /ne/manifest → per-KG status (built | queued + queue_position | stalled | failed | not_in_index). Poll with If-None-Match, honour Retry-After, never treat 404/pending as "empty". Observers: if you assemble cells yourself from the BEV tiles (python package ne_cells, source bevdirect@<tag>, 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 <kg> --observer <label>" (runs bevdirect-serve for you, builds with the bundled ne_cells, posts the report; reads this API tokenless). Change protocol (optional, backwards compatible): (0) header-only report (no chunks{}) → unchanged:true or 202 {need:"chunks"}; (1) report + chunks_ap{} (ne_cells.change.epoch_report_ap: sha256 of each chunk's rows with the register bytes zeroed) → want_chunks[]; (2) POST …/ne/{kg}/chunks?observer= with the NECH body (ne_cells.change.pack_chunk_rows) of exactly those chunks → stored, deltas[]; a report without chunks_ap keeps today's answer. Peers never send register bytes or K rows. Quality control (optional fields, docs/ne-change.md §5a): tiles{expected,fetched} per report — fetched < expected ⇒ change_suspect:"tiles_incomplete", nothing counted; chunks_tiles{chunk: sha256[:16] over the bytes of the source tiles covering it, sorted z/x/y} — a digest that flips while its tile hash stays equal is status "pipeline", a Δ below the quantisation floor (every |Δ| < 5 % of the cell, no dominant-class flip, no count change) is "jitter", two versions of a chunk holding different cell sets (a truncated fetch, two observers' domains — land does not vanish) are "coverage"; none is candidate nor confirmed. /ne/stats.change.qc reports cells_pipeline, cells_jitter, cells_coverage and cluster_share; qc_by_day{day: {cells_pipeline, cells_jitter, cells_coverage}} says what was held back per day (same key as by_day). ## Meta GET /api/v1/license every dataset with licence, attribution text, modified-note, endpoints GET /api/v1/cache/policy client-side caching rules (per-source attribution, TTL recommendations) GET /api/v1/docs/cache-client.py | .js reference caching clients (ETag revalidation, ATTRIBUTION.txt) POST /api/v1/feedback {"title": "...", "agent": "...", "category": "bug|feature|improvement|question", "severity": "critical|high|normal|low", "description": "...", "endpoint": "...", "context": {}} GET /api/v1/feedback?status=open ## Recipe: a map client with its own cadastre (siedler-oesterreich pattern) 1. Draw parcels/footprints/land use from the BEV VTC via vtcseamless / bevdirect-serve (or any BEV-tile assembler). 2. On click, send the parcel's representative point + the features you already hold: GET /api/v1/context?lon=&lat=&area_sqm=&building_count=&footprint_area_sqm=&landuse_areas=&kg= 3. Render data.municipality, data.land_price, data.natura2000/protected_area badges, data.osm distances, data.toponyms names, data.legal; show each block's attribution (meta.license plus the per-source notes in /api/v1/license). 4. For the map background: /api/v1/osm/geometry?bbox=…&cat=road,water,rail&major=1 and /api/v1/toponyms/search?bbox=…&layer=siedlung,ried. If this API is down, only those overlays degrade — your map keeps working. 0. (once) GET /api/v1/kgs → the 7 850-KG universe + universe_hash; key your per-KG state on kg_code and compare universe_hash with srtm-lidar-at's list before joining the two. ## Attribution (short form; full texts in /api/v1/license) Statistik Austria: "Datenquelle: STATISTIK AUSTRIA" (CC BY 4.0; price estimates are modelled) EEA: "Source: European Environment Agency (EEA), Natura 2000" WDPA: UNEP-WCMC & IUCN (non-commercial) OpenStreetMap: "© OpenStreetMap contributors" (ODbL) RIS: "Datenquelle: RIS, Bundeskanzleramt" BEV DLM-Namen: "Datenquelle: BEV – Geographische Namen (DLM), CC BY 4.0, bearbeitet" NE cells: "© BEV (CC BY 4.0), bearbeitet · observed layer © srtm-lidar-at (CC BY 4.0)" — statistics per cell, no object geometry Subsidy layer (glyph only): "INVEKOS Schläge © AMA / BML (CC BY 4.0) · per cell by farm-subsidies-austria.exe.xyz" Sibling services: srtm-lidar-at (https://srtm-lidar-at.exe.xyz:8100/, observed layer per cell, CC BY 4.0) and farm-subsidies-austria (https://farm-subsidies-austria.exe.xyz/, INVEKOS claims per cell, CC BY 4.0) — their own licence pages apply there. This service is not operated by, affiliated with, or endorsed by any of these bodies.