OpenStreetMapbuilding:levelsGeoJSONFeatures APIShadows

How to get building heights and calculate shadows from OpenStreetMap

You cannot tell if a terrace gets sun from a basemap. Vector tiles clip footprints and drop height tags. Fetch full OSM polygons, derive height from tags, then compute shadows.

Try a building queryJoel Sten · Sep 7, 2026How to get building heights and calculate shadows from OpenStreetMap

Want to grab a beer on a sunny terrace? First you need building footprints and heights, unfortunately. Urban Sun Finder does this for pubs, cafes, and restaurants in Nordic winter: fetch building polygons, read height from OSM tags, then test whether a neighbour's shadow reaches the seat.

Vector tiles for basemaps can include building heights, but they are the wrong input. They clip polygons at tile edges, simplify vertices by zoom, and drop important tags. That is fine for drawing a map. It is useless for shadow math. See Features API vs vector tiles for the full comparison. The rest of this post is the recipe: fetch, derive height, then check sun.

Fetch building polygons from OSM

Query closed ways tagged building as unclipped polygons. Height is not a first-class field. It lives in properties.tags on each feature. Ask for centroid=true if you will measure distance from a venue to nearby buildings.

curl Python
curl "https://api.maplark.com/v2/osm_features?bbox=18.06,59.33,18.08,59.34&type=way&way_shape=polygon&tags=building&centroid=true&limit=200" \
  -H "Authorization: Bearer swHAvOmreIm_uew6eqbn1UbIsVQT6p8PRohmqmAJiu4"

Run the same query on the OSM Features playground (buildings preset) or copy the curl into a terminal. The demo key is shared and rate limited; get a free key if you hit 429.

Derive building height from OSM tags

OSM has no single required height tag. Walk the tags in this order and record whether the result is measured or estimated:

  1. Listed height: Check the height and building:height tags. Ground to roof top, metres by default. Convert suffixes such as 18 m or 12 ft before you compute shadows.
  2. Floor count: Estimate height by counting floors. building:levels is facade floors only; roof space is roof:levels. Use 3 to 3.5 m per floor. Check additional tags like levels or building:floors if needed.
    • Formula: (building:levels + roof:levels) * metres_per_level.
  3. Rough guess: est_height when the mapper only estimated.
  4. Your default (for example 10 m) when nothing parses. Flag it as fully estimated.

Three buildings, same block:

  • height=1818 m (measured).
  • building:levels=5, no height17.5 m if you use 3.5 m per floor (estimated).
  • building=yes only - your default, fully estimated. Many buildings look like this.

Prefer height when it disagrees with building:levels. Ignore ele (terrain elevation above sea level) and maxheight (vehicle clearance). Bridges use min_height for the gap underneath; height is still ground to top. Split volumes use building:part. Rooftop bars are a separate query: look for outdoor_seating=roof or location=roof on amenities inside the footprint, then check access=*. A flat roof is not a public terrace.

Check if a place is in sun or shadow

Direct sun is a geometry-and-weather check. Each input answers one question:

  • Horizon: if solar elevation is zero or negative, there is no daylight.
  • Azimuth: does the sun face the seating? If the cafe sits on the north wall and the sun is in the south, the building itself blocks it. Nearby buildings only matter if they sit in that sun direction.
  • Elevation: does not make a place "more sunny." It only sets shadow length: shadow_length = height_above_venue / tan(elevation). Low sun, long shadows. If a taller neighbour is closer than that length, it shades the seat.
  • Cloud cover: a chance of beams, not a guarantee. Geometry can be wide open and still overcast. Clouds never change shadow length.
  • Rain: even with a clear line of sight, outdoor seating is a poor bet.

Takeaway

Fetch full OSM polygons and tags once, derive height in your own code, then compute shadows. That is the gap a basemap tile cannot fill, and why MapLark returns unclipped geometry instead of a pre-rendered layer. Docs for the query parameters are on the developer page.

Run the building query

Open the Features playground, then grab a free API key for your own bbox.

Try it live Get your API key