When building a 3D map of Bangkok with Mapbox, I quickly ran into a data volume problem: almost every building had exactly the same height. That's the default value OpenStreetMap assigns when no height data is available: 4 metres. The result was a perfectly flat city with no urban skyline, where a 30-storey tower looks identical to a two-room house.

Here's the methodology I developed to fix this in QGIS, using open data. It can be reproduced for any city.

A universal problem

Before looking for a solution, a quick audit of Bangkok's OSM buildings:

0.2%of buildings have a height tag different from the 4m default

2.2%have a building:levels tag

84.6%are at the default value of 4 metres

Today, almost all map providers draw from the same OSM data. Coverage is decent in Germany, the Netherlands and a few well-contributed American and Japanese cities. Everywhere else, it's the same story.

The methodology in four steps

The core idea: if a building has no height, identify the administrative district it belongs to, assign it the median height of neighbouring buildings in the same district that do have data, then apply a minimum floor height by building type.

1. Convert levels to heights

OSM contributors often fill in the number of floors via the building:levels tag rather than an absolute height. We can use this data by creating an height_estimate field in the field calculator:

CASE
  WHEN "height" IS NOT NULL THEN to_real("height")
  WHEN "building:levels" IS NOT NULL AND "building" IN ('commercial','retail','office')
       THEN to_real("building:levels") * 3.5
  WHEN "building:levels" IS NOT NULL
       THEN to_real("building:levels") * 3
END

3 metres per floor, 3.5 for commercial types where ceiling heights are greater.

2. Join each building to its subdistrict

Bangkok is divided into administrative subdistricts. The Join attributes by location tool assigns the subdistrict identifier to each building (predicate "is within"). This is the reference unit used for imputation.

3. Impute the median by subdistrict

For each building without a height, we take the median of buildings in the same subdistrict that have a real value (above 4m, to exclude defaults). We only impute if the subdistrict has at least 10 reliable buildings; below that threshold there isn't enough local information to do it without introducing noise. The field calculator handles everything in a single expression using aggregate():

CASE
  WHEN "height_estimate" IS NOT NULL THEN "height_estimate"
  WHEN aggregate('buildings','count',"height_estimate",
        filter:="subdistrict" = attribute(@parent,'subdistrict') AND "height_estimate" > 4) >= 10
  THEN aggregate('buildings','median',"height_estimate",
        filter:="subdistrict" = attribute(@parent,'subdistrict') AND "height_estimate" > 4)
END

I initially set the threshold at 20. Dropping it to 10 quadrupled the number of buildings enriched, from around 20,000 to 88,000.

4. Set a minimum height by building type

Some types have known physical constraints regardless of local context. A school or hotel rarely comes in at 4 metres. We apply a minimum height to remaining buildings, again in the field calculator:

CASE
  WHEN "height_imputed" IS NOT NULL THEN "height_imputed"
  WHEN "building" = 'school'      THEN 12
  WHEN "building" = 'university'  THEN 15
  WHEN "building" = 'hotel'       THEN 24
  WHEN "building" = 'industrial'  THEN 8
  WHEN "building" = 'warehouse'   THEN 8
  WHEN "building" = 'commercial'  THEN 6
  WHEN "building" = 'house'       THEN 6
  WHEN "building" = 'semidetached_house' THEN 6
  WHEN "building" = 'apartments'  THEN 9
  ELSE 4
END

The resulting height_final field can then be used directly for extrusion in QGIS 3D view, or as the height attribute in a web map (Mapbox, MapLibre). The full reference table of heights by building category is at the end of this article.

Results

Buildings at default value: from 84.6% down to 19.1%

Buildings updated: 105,807

Via subdistrict median: 88,273

Via type minimum height: 12,884

Limitations

This is statistical imputation, and it is only useful for improving the visual quality of a 3D map. It is not valid for statistical or scientific work.

The subdistrict median smooths out intra-neighbourhood variation: a 35-storey tower and a two-storey wooden house can coexist 50 metres apart in the same subdistrict, and the median will assign them the same height. The remaining 19% are mostly yes-type buildings with no type or level data, in low-density areas where the threshold of 10 is never reached — often peri-urban zones where 3D rendering is less relevant anyway.

Data and resources

I recommend using Geofabrik to download all OSM buildings. Subdistrict boundaries are also available via OpenStreetMap, on Geofabrik or through an Overpass query.

The Bangkok 3D map that motivated all of this includes a flood risk visualisation by water level across 154,000 buildings, available here.

Reusable reference table

Minimum heights by building type applied in step 4, calibrated on the Bangkok context. Adjust for your city, but usable as a starting point.

Building type Minimum height
school12 m
university15 m
hotel24 m
industrial / warehouse8 m
commercial6 m
house / semidetached_house6 m
apartments9 m