diff --git a/src/simutrans/builder/hausbauer.cc b/src/simutrans/builder/hausbauer.cc
index 1bc93c894..b63995c2a 100644
--- a/src/simutrans/builder/hausbauer.cc
+++ b/src/simutrans/builder/hausbauer.cc
@@ -142,6 +142,11 @@ bool hausbauer_t::successfully_loaded()
 	for(auto const& i : desc_table) {
 		building_desc_t const* const desc = i.value;
 
+		// How tall this building is drawn. Only now: it needs the base tile
+		// raster width, which is set when the "Outside" ground is registered,
+		// and nothing guarantees that happened before this building was read.
+		const_cast<building_desc_t *>(desc)->calc_height_clearance();
+
 		if (desc->is_city_building()) {
 			// find city build sizes
 			if (desc->get_size().x > 3 || desc->get_size().y > 3) {
@@ -886,7 +891,7 @@ const building_desc_t* hausbauer_t::get_special(uint32 bev, building_desc_t::bty
  * @param start_level the minimum level of the house/station
  * @param cl allowed climates
  */
-static const building_desc_t* get_city_building_from_list(const vector_tpl<const building_desc_t*>& list, int start_level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude )
+static const building_desc_t* get_city_building_from_list(const vector_tpl<const building_desc_t*>& list, int start_level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude, sint16 max_clearance )
 {
 	weighted_vector_tpl<const building_desc_t *> selections(16);
 	int level = start_level;
@@ -910,7 +915,12 @@ static const building_desc_t* get_city_building_from_list(const vector_tpl<const
 		     desc->is_available(time)  &&
 		     // size check
 			(desc->get_area()>=minsize  &&  desc->get_area() <= maxsize)  &&
-			(!exclude  ||  !exclude->is_contained(desc))
+			(!exclude  ||  !exclude->is_contained(desc))  &&
+			// a way or bridge deck overhead limits the height: the replacement
+			// must fit under the clearance measured from the planquadrat, so a
+			// building the player could not have built the way over cannot grow
+			// here either. 127 (nothing overhead) admits everything.
+			(desc->get_height_clearance() <= max_clearance)
 		) {
 			desc_at_least = desc;
 			if( thislevel == level ) {
@@ -946,21 +956,21 @@ static const building_desc_t* get_city_building_from_list(const vector_tpl<const
 }
 
 
-const building_desc_t* hausbauer_t::get_commercial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude)
+const building_desc_t* hausbauer_t::get_commercial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude, sint16 max_clearance)
 {
-	return get_city_building_from_list(city_commercial, level, time, cl, clusters, minsize, maxsize, exclude );
+	return get_city_building_from_list(city_commercial, level, time, cl, clusters, minsize, maxsize, exclude, max_clearance );
 }
 
 
-const building_desc_t* hausbauer_t::get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude)
+const building_desc_t* hausbauer_t::get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude, sint16 max_clearance)
 {
-	return get_city_building_from_list(city_industry, level, time, cl, clusters, minsize, maxsize, exclude );
+	return get_city_building_from_list(city_industry, level, time, cl, clusters, minsize, maxsize, exclude, max_clearance );
 }
 
 
-const building_desc_t* hausbauer_t::get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*> *exclude)
+const building_desc_t* hausbauer_t::get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*> *exclude, sint16 max_clearance)
 {
-	return get_city_building_from_list(city_residential, level, time, cl, clusters, minsize, maxsize, exclude );
+	return get_city_building_from_list(city_residential, level, time, cl, clusters, minsize, maxsize, exclude, max_clearance );
 }
 
 
diff --git a/src/simutrans/builder/hausbauer.h b/src/simutrans/builder/hausbauer.h
index e2bba9177..1452b7475 100644
--- a/src/simutrans/builder/hausbauer.h
+++ b/src/simutrans/builder/hausbauer.h
@@ -85,13 +85,13 @@ public:
 	static void fill_menu(tool_selector_t* tool_selector, building_desc_t::btype, waytype_t wt, sint16 sound_ok);
 
 	/// @returns a random commercial building matching the requirements.
-	static const building_desc_t* get_commercial(int level, uint16 time, climate c, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL);
+	static const building_desc_t* get_commercial(int level, uint16 time, climate c, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL, sint16 max_clearance = 127);
 
 	/// @returns a random industrial building matching the requirements.
-	static const building_desc_t* get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL);
+	static const building_desc_t* get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL, sint16 max_clearance = 127);
 
 	/// @returns a random residential building matching the requirements.
-	static const building_desc_t* get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL);
+	static const building_desc_t* get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL, sint16 max_clearance = 127);
 
 	/// @returns headquarters with level @p level (takes the first matching one)
 	static const building_desc_t* get_headquarters(int level, uint16 time);
diff --git a/src/simutrans/builder/tree_builder.cc b/src/simutrans/builder/tree_builder.cc
index 13399c51c..4923cabb3 100644
--- a/src/simutrans/builder/tree_builder.cc
+++ b/src/simutrans/builder/tree_builder.cc
@@ -54,6 +54,13 @@ uint8 tree_builder_t::plant_tree_on_coordinate(koord pos, const uint8 maximum_co
 {
 	grund_t *gr = welt->lookup_kartenboden(pos);
 	if(  gr  ) {
+		// not under an elevated way or bridge deck: a tree carries no height and
+		// the draw clip cannot hide one poking above a low deck (thread 23992),
+		// so it is not planted there in the first place. gr is on the map, so the
+		// plan exists.
+		if(  welt->access(pos)->get_overhead_clearance() < 127  ) {
+			return 0;
+		}
 		if(  has_trees_for_climate( welt->get_climate(pos) )  &&  gr->ist_natur()  &&  gr->obj_count() < maximum_count  ) {
 			obj_t *obj = gr->obj_bei(0);
 			if(obj) {
@@ -99,6 +106,10 @@ bool tree_builder_t::plant_tree_on_coordinate(koord pos, const tree_desc_t *desc
 
 	grund_t *gr = welt->lookup_kartenboden(pos);
 	if(  gr  ) {
+		// not under an elevated way or bridge deck (see the other overload).
+		if(  welt->access(pos)->get_overhead_clearance() < 127  ) {
+			return false;
+		}
 		if(  gr->ist_natur()  &&  gr->obj_count() < welt->get_settings().get_max_no_of_trees_on_square()  &&  (!check_climate  ||  desc->is_allowed_climate( welt->get_climate(pos) ))  ) {
 			if(  gr->obj_count() > 0  ) {
 				switch(gr->obj_bei(0)->get_typ()) {
diff --git a/src/simutrans/builder/wegbauer.cc b/src/simutrans/builder/wegbauer.cc
index ad08374b8..ae45d7ae1 100644
--- a/src/simutrans/builder/wegbauer.cc
+++ b/src/simutrans/builder/wegbauer.cc
@@ -606,7 +606,7 @@ bool way_builder_t::is_allowed_step(const grund_t *from, const grund_t *to, sint
 			if(gb) {
 				// no halt => citybuilding => do not touch
 				// also check for too high buildings ...
-				if(!check_owner(gb->get_owner(),player_builder)  ||  gb->get_tile()->get_background(0,1,0) != IMG_EMPTY) {
+				if(!check_owner(gb->get_owner(),player_builder)  ||  gb->get_tile()->has_upper_storey()) {
 					return false;
 				}
 				// building above houses is expensive ... avoid it!
diff --git a/src/simutrans/descriptor/building_desc.cc b/src/simutrans/descriptor/building_desc.cc
index 164311c1e..48d0e76a4 100644
--- a/src/simutrans/descriptor/building_desc.cc
+++ b/src/simutrans/descriptor/building_desc.cc
@@ -11,9 +11,81 @@
 #include "building_desc.h"
 #include "intro_dates.h"
 #include "../network/checksum.h"
+#include "../display/simgraph.h"
+#include "../dataobj/environment.h"
+#include "../simconst.h"
 
 
 
+/**
+ * How far this tile's artwork rises above its ground line, in height levels.
+ *
+ * The ground line is at half the cell and image coordinates run from the top
+ * left, so one row of artwork reaches (cell/2 - y) above it. Each further image
+ * row is drawn a whole tile higher again (gebaeude_t::display).
+ *
+ * Read from the base images and returned as a count of levels, so the value does
+ * not change with the zoom. Computed, not stored: it is only wanted once, to seed
+ * the building's single height_clearance.
+ */
+uint8 building_tile_desc_t::get_drawn_height() const
+{
+	uint8 height_levels = 0;
+
+	const sint16 cell = gfx->get_base_tile_raster_width();
+	// image pixels per height level. Not the raw tile_height from simuconf.tab:
+	// pak128 says 8 and pak128.german says 16, and both are 128 wide.
+	const sint16 level_h = max((sint16)1, (sint16)tile_raster_scale_y(TILE_HEIGHT_STEP, cell));
+
+	for(  uint8 season = 0;  season < seasons;  season++  ) {
+		image_array_t const* const imglist = get_child<image_array_t>(0 + 2 * season);
+		if(  imglist == NULL  ) {
+			continue;
+		}
+
+		// topmost row holding an image, and how far the ground row reaches up
+		sint16 rows  = 0;
+		sint16 above = 0;
+		for(  uint16 h = 0;  h < imglist->get_count();  h++  ) {
+			if(  image_t const* const image = imglist->get_image(h, 0)  ) {
+				rows = h + 1;
+				if(  h == 0  ) {
+					above = cell / 2 - image->y;
+				}
+			}
+		}
+		if(  rows == 0  ) {
+			continue;
+		}
+
+		// artwork that stays below the ground line does not rise above it at all
+		const sint16 levels = ((rows - 1) * cell + max((sint16)0, above)) / level_h;
+
+		if(  levels > height_levels  ) {
+			height_levels = (uint8)min(levels, (sint16)255);
+		}
+	}
+
+	return height_levels;
+}
+
+
+void building_desc_t::calc_height_clearance()
+{
+	// One height for the whole building, the tallest of its tiles. The draw path
+	// reads this single value for every tile (prissi, thread 23991): a per-tile
+	// lookup there is a cache miss per tile.
+	height_clearance = 0;
+
+	for(  uint16 i = 0;  i < (uint16)(layouts * size.x * size.y);  i++  ) {
+		const uint8 h = get_tile(i)->get_drawn_height();
+		if(  h > height_clearance  ) {
+			height_clearance = h;
+		}
+	}
+}
+
+
 /**
  * Calculate which layout the tile belongs to from the index.
  */
diff --git a/src/simutrans/descriptor/building_desc.h b/src/simutrans/descriptor/building_desc.h
index 9130a875d..abc1a01d6 100644
--- a/src/simutrans/descriptor/building_desc.h
+++ b/src/simutrans/descriptor/building_desc.h
@@ -46,10 +46,36 @@ public:
 	uint8 get_seasons() const { return seasons; }
 	uint8 get_phases() const { return phases; }
 
+	/**
+	 * How far this tile's artwork rises above its ground line, in HEIGHT LEVELS.
+	 * Computed from the images, not read off the image list, because:
+	 *
+	 * - the height index of get_background() is not a height level. Each further
+	 *   image row lifts the artwork by a whole tile (gebaeude_t::display does
+	 *   ypos -= raster_width), which is eight levels in pak128;
+	 * - within a row, the artwork's own top edge matters, because makeobj crops
+	 *   every image to its opaque bounding box and stores where that box was
+	 *   (image_writer.cc). A single-row building can still be several levels tall.
+	 *
+	 * Needs the base tile raster width, so it can only be called once the pakset
+	 * is fully loaded (the "Outside" ground sets it). Used once, to seed the
+	 * building's height_clearance; the draw path never calls this.
+	 */
+	uint8 get_drawn_height() const;
+
 	bool has_image() const {
 		return get_background(0,0,0)!=IMG_EMPTY  ||  get_foreground(0,0)!=IMG_EMPTY;
 	}
 
+	/**
+	 * Does this tile draw anything above the ground storey?
+	 * This is the test that decides whether an elevated way may pass over the
+	 * building, so it lives here rather than being spelled out at each use.
+	 */
+	bool has_upper_storey() const {
+		return get_background(0, 1, 0) != IMG_EMPTY;
+	}
+
 	image_id get_background(uint16 phase, uint16 height, uint8 season) const
 	{
 		image_array_t const* const imglist = get_child<image_array_t>(0 + 2 * season);
@@ -209,6 +235,9 @@ private:
 
 	uint16 preservation_year_month;
 
+	/// @see get_height_clearance
+	uint8 height_clearance;
+
 	bool is_type(building_desc_t::btype u) const {
 		return type == u;
 	}
@@ -285,8 +314,38 @@ public:
 		return get_child<building_tile_desc_t>(index + 2);
 	}
 
+	/**
+	 * The clearance this building needs above its ground, in height levels: the
+	 * tallest of its tiles, over every layout.
+	 *
+	 * It is the whole building and not the single tile on purpose. A tile that
+	 * draws nothing above the ground may still belong to a tower, and asking
+	 * only the tile a way happens to cross lets the way through the low corner
+	 * of a tall building. In pak128.german, 90 of the 128 multi-tile city
+	 * buildings have a one-row tile beside a taller one.
+	 */
+	uint8 get_height_clearance() const { return height_clearance; }
+
+	/// Derive get_height_clearance() from the tiles. @see building_tile_desc_t::calc_height_levels
+	void calc_height_clearance();
+
 	const building_tile_desc_t *get_tile(uint8 layout, sint16 x, sint16 y) const;
 
+	/**
+	 * Does any tile of this building draw above the ground storey?
+	 * An elevated way cannot pass over such a building (see way_builder_t),
+	 * so a replacement chosen for a spot that already has one overhead must
+	 * not have an upper storey either.
+	 */
+	bool has_upper_storey() const {
+		for(  uint16 i = 0;  i < layouts * size.x * size.y;  i++  ) {
+			if(  get_tile(i)->has_upper_storey()  ) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	// returns true,if building can be rotated
 	bool can_rotate() const {
 		if(size.x!=size.y  &&  layouts==1) {
diff --git a/src/simutrans/world/simcity.cc b/src/simutrans/world/simcity.cc
index 14eb83e9f..8060e50c0 100644
--- a/src/simutrans/world/simcity.cc
+++ b/src/simutrans/world/simcity.cc
@@ -521,6 +521,15 @@ class monument_placefinder_t : public placefinder_t {
 				return false;
 			}
 
+			// not under an elevated way or bridge deck. A monument is a showcase
+			// object that belongs on open ground; the draw clip cannot hide the
+			// part poking above a low deck (thread 23992), and its unusual art
+			// makes a per-height test unreliable - so keep it off overhead tiles
+			// entirely rather than trust a measured height.
+			if(  plan->get_overhead_clearance() < 127  ) {
+				return false;
+			}
+
 			if (is_boundary_tile(d)) {
 				return
 					gr->get_grund_hang() == slope_t::flat &&     // Flat
@@ -3391,19 +3400,27 @@ void stadt_t::build_city_building(koord k_org)
 	const uint16 current_month = welt->get_timeline_year_month();
 	const climate cl = welt->get_climate(k);
 
+	// How much clear height is above this tile - same limit renovation uses, so a
+	// city expanding under an elevated way or bridge deck also only places a
+	// building that fits under it. 127 = nothing overhead, no limit.
+	sint16 max_clearance = 127;
+	if(  const planquadrat_t *plan = welt->access(k)  ) {
+		max_clearance = plan->get_overhead_clearance();
+	}
+
 	// Find a house to build
 	const building_desc_t* h = NULL;
 
 	if (sum_commercial > sum_industrial && sum_commercial >= sum_residential) {
-		h = hausbauer_t::get_commercial(0, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_commercial(0, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, max_clearance);
 	}
 
 	if (h == NULL && sum_industrial > sum_residential && sum_industrial >= sum_commercial) {
-		h = hausbauer_t::get_industrial(0, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_industrial(0, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, max_clearance);
 	}
 
 	if (h == NULL && sum_residential > sum_industrial && sum_residential >= sum_commercial) {
-		h = hausbauer_t::get_residential(0, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_residential(0, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, max_clearance);
 	}
 
 	if (h == NULL) {
@@ -3505,11 +3522,23 @@ bool stadt_t::renovate_city_building(gebaeude_t *gb)
 	building_desc_t::btype want_to_have = building_desc_t::unknown;
 	int sum = 0;
 
+	// How much clear height is above this tile, in levels. An elevated way or a
+	// bridge deck sitting a few levels up limits how tall a replacement may be:
+	// a building the player could never have built the way over must not appear
+	// by renovation either. Reading it off the planquadrat is cheap - the ground
+	// stack is short and already in cache (prissi, thread 23991). A taller deck
+	// admits taller buildings on its own, which is why a numeric limit is more
+	// future proof than a yes/no test. 127 = nothing overhead, no limit.
+	sint16 max_clearance = 127;
+	if(  const planquadrat_t *plan = welt->access(k)  ) {
+		max_clearance = plan->get_overhead_clearance();
+	}
+
 	// try to build
 	const building_desc_t* h = NULL;
 	if (sum_commercial > sum_industrial && sum_commercial > sum_residential) {
 		// we must check, if we can really update to higher level ...
-		h = hausbauer_t::get_commercial(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_commercial(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, max_clearance);
 		if(  h != NULL  &&  h->get_level() >= level+1  ) {
 			want_to_have = building_desc_t::city_com;
 			sum = sum_commercial;
@@ -3519,7 +3548,7 @@ bool stadt_t::renovate_city_building(gebaeude_t *gb)
 	if(    (sum_industrial > sum_commercial  &&  sum_industrial > sum_residential) ||
 	       (sum_commercial > sum_residential  &&  want_to_have == building_desc_t::unknown)  ) {
 		// we must check, if we can really update to higher level ...
-		h = hausbauer_t::get_industrial(level+1 , current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_industrial(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, max_clearance);
 		if(  h != NULL  &&  h->get_level() >= level+1  ) {
 			want_to_have = building_desc_t::city_ind;
 			sum = sum_industrial;
@@ -3529,7 +3558,7 @@ bool stadt_t::renovate_city_building(gebaeude_t *gb)
 	// (sum_wohnung>sum_industrie  &&  sum_wohnung>sum_gewerbe
 	if(  want_to_have == building_desc_t::unknown  ) {
 		// we must check, if we can really update to higher level ...
-		h = hausbauer_t::get_residential(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_residential(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, max_clearance);
 		if(  h != NULL  &&  h->get_level() >= level+1  ) {
 			want_to_have = building_desc_t::city_res;
 			sum = sum_residential;
diff --git a/src/simutrans/world/simplan.cc b/src/simutrans/world/simplan.cc
index 01d5b8e0a..455ec2acc 100644
--- a/src/simutrans/world/simplan.cc
+++ b/src/simutrans/world/simplan.cc
@@ -87,6 +87,20 @@ grund_t *planquadrat_t::get_boden_von_obj(obj_t *obj) const
 }
 
 
+sint16 planquadrat_t::get_overhead_clearance() const
+{
+	sint16 clearance = 127;
+	const sint8 base_h = get_kartenboden()->get_hoehe();
+	for(  unsigned b = 1;  b < get_boden_count();  b++  ) {
+		const sint8 diff_h = get_boden_bei(b)->get_hoehe() - base_h;
+		if(  diff_h > 0  &&  diff_h < clearance  ) {
+			clearance = diff_h;
+		}
+	}
+	return clearance;
+}
+
+
 void planquadrat_t::boden_hinzufuegen(grund_t *bd)
 {
 	assert(!bd->ist_karten_boden());
@@ -484,17 +498,45 @@ void planquadrat_t::display_obj(const sint16 xpos, const sint16 ypos, const sint
 		// clip everything at the next tile above
 		if(  i < ground_size  ) {
 			// there are grounds above => check height of height object below
+			// How far the objects on this tile rise above its ground, in HEIGHT
+			// LEVELS - which is the unit (htop - hmin) is measured in below.
 			sint16 max_height = 0;
-			sint16 rw = gfx->get_base_tile_raster_width();
-			const sint16 clip_h = rw / 2 + (env_t::pak_height_conversion_factor * rw / 8);
+			// get_image_offset() returns the zoomed offsets, so take the raster
+			// width zoomed too; the quotient is the same count either way.
+			const sint16 rw = gfx->get_tile_raster_width();
+			const sint16 level_h = max((sint16)1, (sint16)tile_raster_scale_y(TILE_HEIGHT_STEP, rw));
 
 			for (uint8 i = 0; i < gr0->obj_count(); i++) {
 				obj_t* o = gr0->obj_bei(i);
-				if (o->get_typ() == obj_t::baum   ||  dynamic_cast<gebaeude_t *>(o)) {
+				// This is a hot loop - the draw path runs it for every tile.
+				// get_typ() + static_cast, never dynamic_cast, and a single
+				// cached height off the desc, never a per-tile computation
+				// (prissi, thread 23991: almost every tile is a cache miss).
+				if (o->get_typ() == obj_t::gebaeude) {
+					const gebaeude_t *gb = static_cast<const gebaeude_t *>(o);
+					// the building's get_image() is its ground row alone, so
+					// measuring that image would call a tower one storey tall;
+					// the height was computed once at load.
+					max_height = max(max_height, (sint16)gb->get_tile()->get_desc()->get_height_clearance());
+				}
+				else if (o->get_typ() == obj_t::baum) {
 					image_id img = o->get_image();
 					if (img != IMG_EMPTY) {
 						const scr_rect area = gfx->get_image_offset(img);
-						max_height = max(max_height, (area.h - area.y) / clip_h);
+						// The ground line is at half the cell and image
+						// coordinates run from the top left, so the artwork
+						// reaches (rw/2 - y) above it. What stood here,
+						// (area.h - area.y), subtracted an offset from a size:
+						// negative for 144 of pak128's 550 tree images.
+						// A tree also carries a per-instance offset (baum_t::
+						// calc_off): a negative yoff lifts it toward the top
+						// corner, so its real screen-top rises higher than the
+						// sprite box alone. Fold in the same scaled yoff the draw
+						// path applies (simobj.cc: ypos += scale(get_yoff())), or
+						// a top-corner tree slips through the gate below (prissi,
+						// thread 23992).
+						const sint16 yoff = tile_raster_scale_y(o->get_yoff(), rw);
+						max_height = max(max_height, max((sint16)0, (sint16)(rw / 2 - area.y - yoff)) / level_h);
 					}
 				}
 			}
diff --git a/src/simutrans/world/simplan.h b/src/simutrans/world/simplan.h
index debbe54e2..8c0fd1490 100644
--- a/src/simutrans/world/simplan.h
+++ b/src/simutrans/world/simplan.h
@@ -135,6 +135,14 @@ public:
 	/// @returns number of grounds on this map square.
 	unsigned int get_boden_count() const { return ground_size; }
 
+	/**
+	* How much clear height there is above the kartenboden, in height levels:
+	* the smallest positive gap to a ground stacked above it (an elevated way or
+	* bridge deck). Returns 127 when nothing is overhead. Used to stop tall
+	* objects being placed where they would poke through a deck.
+	*/
+	sint16 get_overhead_clearance() const;
+
 	/**
 	* returns climate of plan (lowest 3 bits of climate byte)
 	*/
