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 query 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 "https://api.maplark.com/v2/osm_features?bbox=18.06,59.33,18.08,59.34&type=way&way_shape=polygon&tags=building¢roid=true&limit=200" \
-H "Authorization: Bearer swHAvOmreIm_uew6eqbn1UbIsVQT6p8PRohmqmAJiu4"# pip install osmfeatures
from osmfeatures import OSMFeaturesClient
with OSMFeaturesClient(api_key="swHAvOmreIm_uew6eqbn1UbIsVQT6p8PRohmqmAJiu4") as client:
fc = client.query(
bbox="18.06,59.33,18.08,59.34",
type=["way"],
way_shape="polygon",
tags=["building"],
centroid=True,
limit=200,
)
print(len(fc.features), "buildings")
print(fc.features[0].tags) 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:
- Listed height: Check the
heightandbuilding:heighttags. Ground to roof top, metres by default. Convert suffixes such as18 mor12 ftbefore you compute shadows. - Floor count: Estimate height by counting floors.
building:levelsis facade floors only; roof space isroof:levels. Use 3 to 3.5 m per floor. Check additional tags likelevelsorbuilding:floorsif needed.- Formula:
(building:levels + roof:levels) * metres_per_level.
- Formula:
- Rough guess:
est_heightwhen the mapper only estimated. - Your default (for example 10 m) when nothing parses. Flag it as fully estimated.
Three buildings, same block:
height=18→ 18 m (measured).building:levels=5, noheight→ 17.5 m if you use 3.5 m per floor (estimated).building=yesonly - 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.