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/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..b260cef12 100644
--- a/src/simutrans/world/simcity.cc
+++ b/src/simutrans/world/simcity.cc
@@ -3505,11 +3505,29 @@ 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)  ) {
+		const sint8 base_h = plan->get_kartenboden()->get_hoehe();
+		for(  unsigned b = 1;  b < plan->get_boden_count();  b++  ) {
+			const sint8 diff_h = plan->get_boden_bei(b)->get_hoehe() - base_h;
+			if(  diff_h > 0  &&  diff_h < max_clearance  ) {
+				max_clearance = diff_h;
+			}
+		}
+	}
+
 	// 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 +3537,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 +3547,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..6617b58b2 100644
--- a/src/simutrans/world/simplan.cc
+++ b/src/simutrans/world/simplan.cc
@@ -484,17 +484,37 @@ 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.
+						max_height = max(max_height, max((sint16)0, (sint16)(rw / 2 - area.y)) / level_h);
 					}
 				}
 			}
