Skip to content

feat: serve roof and wall surfaces for a bbox, paged - #219

Merged
jravani merged 2 commits into
mainfrom
feat/bulk-surfaces
Oct 25, 2026
Merged

jravani merged 2 commits into
mainfrom
feat/bulk-surfaces

Conversation

@jravani

@jravani jravani commented Oct 24, 2026

Copy link
Copy Markdown
Member

Rendering the LoD2 roofs and walls of an area meant calling GET /api/v1/geometry?include=surfaces once per building: that endpoint allows one object_id per request, because a building carries a handful to over 500 faces and a building count does not bound the response size.

Change

GET /api/v1/surfaces?country=..&xmin=..&ymin=..&xmax=..&ymax=..[&cursor=..]

{
  "buildings": [
    {"object_id": "DEBY_LOD2_7672144",
     "osm_ids": ["223728275"],
     "surfaces": [
       {"id": "014336c5-…", "type": "RoofSurface",
        "geometry": {"type": "Polygon", "coordinates": [[[12.9512345, 48.8312345, 321.42], …]]}}]}],
  "attributions": [ … ],
  "next_cursor": "REVCWV9MT0QyXzc2NzM3MDM"
}
  • Every building whose footprint intersects the WGS84 bbox, with its OSM ways best first ([] when unlinked).
  • Each roof and wall surface: the same id as /geometry, its type, and a GeoJSON Polygon in EPSG:4326 with height, 7 decimals. Ground surfaces are left out; party-wall pieces are included, so attached buildings render closed.
  • Paging: whole buildings in object_id order, up to 75,000 surface vertices per page, which is about 4 MB at the measured 52 bytes per vertex. Every page holds at least one building. One query picks the page by a running sum of vertices, so geometry is encoded only for the page's buildings. next_cursor (an encoded object_id) is null on the last page.
  • 400 for a missing country or bbox, a non-numeric bbox value, or a cursor the API did not return. An unconfigured country gives empty lists.
  • /geometry is unchanged.
  • Docs: OpenAPI (path, SurfacesResponse, BuildingSurfaces, RenderSurface), CHANGELOG.md.

Tests

  • Unit: cursor round trip and rejection; the handler's 400s, decided before any database lookup.

  • Integration, TestSurfacesInBBox_PagesCoverEveryBuildingOnce on the real Bavarian seed with a 300-vertex budget:

    • every building of the bbox arrives exactly once, over several pages;
    • no page exceeds the budget unless it holds a single building;
    • only roof and wall surfaces, as 3D WGS84 polygons;
    • surface ids equal /geometry's.

    With the cursor made inclusive it fails: still paging after 261 pages for 259 buildings.

  • go vet ./... and go test ./... pass.

  • End to end on Deggendorf and Koeln (27,420 buildings):

    • the whole area comes in 28 pages of at most 4.06 MB, 0.06 to 0.90 s each, every building once;
    • a small box around the THD campus filled its first page with 667 buildings, 3.86 MB, in 0.17 s, and returned a next_cursor for the rest.

Rendering the LoD2 roofs and walls of an area meant calling
GET /api/v1/geometry?include=surfaces once per building, as that
endpoint allows one object_id per request. A building carries a
handful to over 500 faces, so a building count does not bound the
response size, and clients with a response cap (10 MiB, for example)
can be overrun.

GET /api/v1/surfaces takes a country and a WGS84 bbox and returns
every building whose footprint intersects it: its object_id, its OSM
ways best first, and each roof and wall surface with the same id as
/geometry's, its type and a GeoJSON Polygon in EPSG:4326 with height
to 7 decimals. Ground surfaces are left out; party-wall pieces are in,
so attached buildings render closed.

Pages hold whole buildings in object_id order up to 75,000 surface
vertices, about 4 MB at the measured 52 bytes per vertex, and at least
one building. One query picks the page by a running sum of vertices,
so geometry is encoded only for the page's buildings. next_cursor, an
encoded object_id, is null on the last page.

On Deggendorf and Koeln (27,420 buildings) the whole area came in 28
pages of at most 4.06 MB, 0.06 to 0.90 s each, every building once.
GET /api/v1/surfaces returns absolute WGS84 coordinates with no origin
subtracted. A float32 latitude near 49 degrees resolves to about 0.4 m,
so a renderer working in 32-bit floats subtracts its own origin first;
the OpenAPI geometry description now says so. The endpoint description
also states that its surfaces are those of /geometry?include=surfaces
without ground surfaces.
@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.21622% with 50 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/onrequest/surfaces.go 74.33% 19 Missing and 10 partials ⚠️
internal/api/handler/surfaces.go 38.23% 21 Missing ⚠️

📢 Thoughts on this report? Let us know!

@jravani jravani added this to the v0.13.0 milestone Oct 24, 2026
@jravani
jravani merged commit 15b2b1f into main Oct 25, 2026
7 checks passed
@jravani
jravani deleted the feat/bulk-surfaces branch October 25, 2026 08:32
@jravani jravani mentioned this pull request Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant