diff --git a/Makefile b/Makefile
index f7b383c02..6805edd32 100644
--- a/Makefile
+++ b/Makefile
@@ -294,6 +294,7 @@ CCFLAGS  += -ansi -Wstrict-prototypes -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
 
 # sources (in alphabetic order)
 SOURCES += src/simutrans/builder/brueckenbauer.cc
+SOURCES += src/simutrans/builder/double_way_builder.cc
 SOURCES += src/simutrans/builder/fabrikbauer.cc
 SOURCES += src/simutrans/builder/goods_manager.cc
 SOURCES += src/simutrans/builder/hausbauer.cc
diff --git a/cmake/SimutransSourceList.cmake b/cmake/SimutransSourceList.cmake
index 33d3044db..a9f08de13 100644
--- a/cmake/SimutransSourceList.cmake
+++ b/cmake/SimutransSourceList.cmake
@@ -1,5 +1,6 @@
 target_sources(simutrans PRIVATE
 		src/simutrans/builder/brueckenbauer.cc
+		src/simutrans/builder/double_way_builder.cc
 		src/simutrans/builder/fabrikbauer.cc
 		src/simutrans/builder/goods_manager.cc
 		src/simutrans/builder/hausbauer.cc
diff --git a/simutrans/text/en.tab b/simutrans/text/en.tab
index 0e8333d4b..94bc8b40a 100644
--- a/simutrans/text/en.tab
+++ b/simutrans/text/en.tab
@@ -513,6 +513,10 @@ build choosesignals
 Build platform choose signals
 Build city market
 Build a new market in the nearest city.
+Build double way left
+Build two parallel ways, the second one on the left
+Build double way right
+Build two parallel ways, the second one on the right
 Build drain
 Transformer station
 build HQ
diff --git a/src/simutrans/builder/wegbauer.cc b/src/simutrans/builder/wegbauer.cc
index aef1b28a1..ad4c9436f 100644
--- a/src/simutrans/builder/wegbauer.cc
+++ b/src/simutrans/builder/wegbauer.cc
@@ -16,6 +16,7 @@
 #include "../obj/depot.h"
 
 #include "wegbauer.h"
+#include "double_way_builder.h"
 #include "brueckenbauer.h"
 #include "tunnelbauer.h"
 
@@ -322,6 +323,31 @@ void way_builder_t::fill_menu(tool_selector_t *tool_selector, const waytype_t wt
 		i->get_builder()->enabled = enable  && welt->get_scenario()->is_tool_enabled(welt->get_active_player(), TOOL_BUILD_WAY | GENERAL_TOOL, rwtyp, i->get_name());
 		tool_selector->add_tool_selector(i->get_builder());
 	}
+
+	/* The two double way buttons are appended from here rather than from
+	 * menuconf.tab: the toolbar layout is pakset data, so a menuconf entry would
+	 * only ever reach the pakset it was written for. Added in C++ they show up in
+	 * every pakset, in the same toolbar as the ways they build, and only for the
+	 * waytypes double_way_builder_t accepts.
+	 * One pair per waytype, built on first use and kept for the session, the same
+	 * way register_desc() keeps one builder tool per way descriptor. The parameter
+	 * is the numeric form, so the button follows the player's default way for that
+	 * type; get_default_param() resolves it to a name before anything is sent.
+	 */
+	if(  !matching.empty()  &&  double_way_builder_t::is_supported( matching[0] )  ) {
+		static tool_build_way_double_t *double_tools[2][64] = { };
+		const uint8 wt_index = wtyp & 63;
+		for(  uint8 s = 0;  s < 2;  s++  ) {
+			tool_build_way_double_t *&tool = double_tools[s][wt_index];
+			if(  tool == NULL  ) {
+				tool = new tool_build_way_double_t();
+				// the tool owns the parameter string, so the menu allocates nothing
+				tool->set_menu_param( s == 0 ? double_way_builder_t::side_left : double_way_builder_t::side_right, wtyp );
+			}
+			tool->enabled = enable;
+			tool_selector->add_tool_selector( tool );
+		}
+	}
 }
 
 
@@ -556,6 +582,13 @@ grund_t *way_builder_t::find_base_for_elevated(const koord3d &upper_pos) const
  */
 bool way_builder_t::is_allowed_step(const grund_t *from, const grund_t *to, sint32 *costs, bool is_upperlayer )
 {
+	// Tiles the caller ruled out are simply not there for this search. Checked
+	// first and without touching *costs, so a forbidden tile can never be traded
+	// back in by any cost setting. Skipped entirely while the list is empty.
+	if(  !forbidden_tiles.empty()  &&  forbidden_tiles.is_contained( to->get_pos() )  ) {
+		return false;
+	}
+
 	const koord from_pos = from->get_pos().get_2d();
 	const koord to_pos   = to->get_pos().get_2d();
 	const koord zv       = to_pos-from_pos;
@@ -1312,6 +1345,15 @@ way_builder_t::way_builder_t(player_t* player)
 }
 
 
+void way_builder_t::set_forbidden_tiles(const vector_tpl<koord3d> &tiles)
+{
+	forbidden_tiles.clear();
+	for(  koord3d const &pos : tiles  ) {
+		forbidden_tiles.append_unique(pos);
+	}
+}
+
+
 /**
  * If a way is built on top of another way, should the type
  * of the former way be kept or replaced (true == keep)
diff --git a/src/simutrans/builder/wegbauer.h b/src/simutrans/builder/wegbauer.h
index d6ddaa767..f270372dc 100644
--- a/src/simutrans/builder/wegbauer.h
+++ b/src/simutrans/builder/wegbauer.h
@@ -122,6 +122,9 @@ private:
 	// try to keep a way close to existing ways
 	bool prefer_parallel;
 
+	/// tiles this search must not enter; empty means no restriction at all
+	vector_tpl<koord3d> forbidden_tiles;
+
 	bool build_sidewalk;
 
 	sint32 maximum;    // highest cost
@@ -204,6 +207,16 @@ public:
 		prefer_parallel = yesno;
 	}
 
+	/**
+	 * Tiles the route search may not use. They are rejected outright by
+	 * is_allowed_step(), not made expensive, so no cost setting can trade them
+	 * back in. The list is copied and belongs to this builder only.
+	 * While it is empty (the default) the search behaves exactly as before.
+	 */
+	void set_forbidden_tiles(const vector_tpl<koord3d> &tiles);
+	void clear_forbidden_tiles() { forbidden_tiles.clear(); }
+	void forbid_tile(const koord3d &pos) { forbidden_tiles.append_unique(pos); }
+
 	/**
 	 * If a way is built on top of another way, should the type
 	 * of the former way be kept or replaced (true == keep)
diff --git a/src/simutrans/script/api/api_const.cc b/src/simutrans/script/api/api_const.cc
index 1142da1ac..35036bfdd 100644
--- a/src/simutrans/script/api/api_const.cc
+++ b/src/simutrans/script/api/api_const.cc
@@ -53,6 +53,8 @@ void export_global_constants(HSQUIRRELVM vm)
 	enum_slot(vm, "tool_build_groundobj", TOOL_PLANT_GROUNDOBJ | GENERAL_TOOL);
 	/// build ways
 	enum_slot(vm, "tool_build_way", TOOL_BUILD_WAY | GENERAL_TOOL);
+	/// build two parallel ways at once, default_param is "<side>,<way name>" with side 'l' or 'r'
+	enum_slot(vm, "tool_build_way_double", TOOL_BUILD_WAY_DOUBLE | GENERAL_TOOL);
 	/// build bridges
 	enum_slot(vm, "tool_build_bridge", TOOL_BUILD_BRIDGE | GENERAL_TOOL);
 	/// build tunnel
diff --git a/src/simutrans/script/api/api_pathfinding.cc b/src/simutrans/script/api/api_pathfinding.cc
index fcf6f74ab..c5b61b93f 100644
--- a/src/simutrans/script/api/api_pathfinding.cc
+++ b/src/simutrans/script/api/api_pathfinding.cc
@@ -111,6 +111,16 @@ void way_builder_set_build_types(way_builder_t *bob, const way_desc_t *way)
 	}
 }
 
+void way_builder_forbid_tile(way_builder_t *bob, koord3d pos)
+{
+	bob->forbid_tile(pos);
+}
+
+void way_builder_clear_forbidden_tiles(way_builder_t *bob)
+{
+	bob->clear_forbidden_tiles();
+}
+
 bool way_builder_is_allowed_step(way_builder_t *bob, grund_t *from, grund_t *to)
 {
 	if (from == NULL || to == NULL) {
@@ -202,6 +212,17 @@ void export_pathfinding(HSQUIRRELVM vm)
 	 */
 	register_method(vm, way_builder_is_allowed_step, "is_allowed_step", true);
 
+	/**
+	 * Forbids one tile: @ref is_allowed_step will refuse to enter it, and the
+	 * route search will not use it. Only affects this planner.
+	 * @param pos tile to forbid
+	 */
+	register_method(vm, way_builder_forbid_tile, "forbid_tile", true);
+	/**
+	 * Drops all tiles forbidden by @ref forbid_tile, restoring the normal search.
+	 */
+	register_method(vm, way_builder_clear_forbidden_tiles, "clear_forbidden_tiles", true);
+
 	end_class(vm);
 
 	/**
diff --git a/src/simutrans/tool/simmenu.cc b/src/simutrans/tool/simmenu.cc
index 8f219db4f..06ad9f1f2 100644
--- a/src/simutrans/tool/simmenu.cc
+++ b/src/simutrans/tool/simmenu.cc
@@ -250,6 +250,7 @@ tool_t* create_general_tool(int toolnr)
 	case TOOL_SCHEDULE_ADD:                tool = new tool_schedule_add_t();        break;
 	case TOOL_SCHEDULE_INS:                tool = new tool_schedule_ins_t();        break;
 	case TOOL_BUILD_WAY:                   tool = new tool_build_way_t();           break;
+	case TOOL_BUILD_WAY_DOUBLE:            tool = new tool_build_way_double_t();    break;
 	case TOOL_BUILD_BRIDGE:                tool = new tool_build_bridge_t();        break;
 	case TOOL_BUILD_TUNNEL:                tool = new tool_build_tunnel_t();        break;
 	case TOOL_WAYREMOVER:                  tool = new tool_wayremover_t();          break;
diff --git a/src/simutrans/tool/simmenu.h b/src/simutrans/tool/simmenu.h
index 756b41cf1..8935e2a0f 100644
--- a/src/simutrans/tool/simmenu.h
+++ b/src/simutrans/tool/simmenu.h
@@ -82,6 +82,7 @@ enum {
 	TOOL_GENERATE_SCRIPT,
 	TOOL_PIPETTE,
 	TOOL_SET_OWNER,
+	TOOL_BUILD_WAY_DOUBLE,
 	GENERAL_TOOL_COUNT,
 	GENERAL_TOOL = 0x1000
 };
@@ -320,6 +321,14 @@ public:
 	// will draw a dark frame, if selected
 	virtual void draw_after(scr_coord pos, bool dirty) const;
 
+	/**
+	 * Tool id a scenario sees when it is asked whether this tool may be used.
+	 * Defaults to the real id. A tool that is a variant of another one reports
+	 * the id it behaves as, so a scenario that forbids the original does not
+	 * have to know about the variant.
+	 */
+	virtual uint16 get_scenario_tool_id() const { return get_id(); }
+
 	virtual const char *get_tooltip(const player_t *) const { return NULL; }
 
 	/**
diff --git a/src/simutrans/tool/simtool.cc b/src/simutrans/tool/simtool.cc
index afaf9d5b0..788387632 100644
--- a/src/simutrans/tool/simtool.cc
+++ b/src/simutrans/tool/simtool.cc
@@ -81,6 +81,7 @@
 #include "../builder/tunnelbauer.h"
 #include "../builder/brueckenbauer.h"
 #include "../builder/wegbauer.h"
+#include "../builder/double_way_builder.h"
 #include "../builder/hausbauer.h"
 
 #include "../descriptor/way_desc.h"
@@ -3075,6 +3076,211 @@ const char* tool_build_cityroad::do_work(player_t* player, const koord3d& start,
 }
 
 
+/* double way construction */
+const way_desc_t *tool_build_way_double_t::get_desc() const
+{
+	// default_param is "<side>,<way name>" or "<side>,<waytype number>"; the
+	// numeric form follows the player's default way for that type, exactly like
+	// the "build default way" buttons of the ordinary way tool do
+	if(  default_param == NULL  ) {
+		return NULL;
+	}
+	const char *n = strchr( default_param, ',' );
+	if(  n == NULL  ) {
+		return NULL;
+	}
+	n++;
+	if(  const way_desc_t *found = way_builder_t::get_desc( n, 0 )  ) {
+		return found;
+	}
+	const waytype_t wt = (waytype_t)atoi( n );
+	return wt > ignore_wt ? get_default_desc( wt ) : NULL;
+}
+
+
+void tool_build_way_double_t::set_menu_param( uint8 s, waytype_t wt )
+{
+	sprintf( own_param, "%c,%d", s == double_way_builder_t::side_left ? 'l' : 'r', (int)wt );
+	set_default_param( own_param );
+}
+
+
+image_id tool_build_way_double_t::get_icon( player_t * ) const
+{
+	const way_desc_t *d = get_desc();
+	if(  d == NULL  ||  !double_way_builder_t::is_supported( d )  ) {
+		return IMG_EMPTY;
+	}
+	if(  grund_t::underground_mode == grund_t::ugm_all  ||  !d->is_available( welt->get_timeline_year_month() )  ) {
+		return IMG_EMPTY;
+	}
+	// PLACEHOLDER ART: both sides borrow the icon of the way itself, so the two
+	// buttons look alike and are told apart only by their tooltip. Replace with
+	// two dedicated pakset images when they exist; nothing else depends on this.
+	return icon != IMG_EMPTY ? icon : d->get_cursor()->get_image_id(1);
+}
+
+
+const char *tool_build_way_double_t::get_tooltip( const player_t * ) const
+{
+	const way_desc_t *d = get_desc();
+	if(  d == NULL  ) {
+		return "";
+	}
+	// two ways are laid per tile of the drag, so the price and the upkeep shown
+	// here are the ones for a tile of double track
+	tooltip_with_price_maintenance( welt,
+		side == double_way_builder_t::side_left ? "Build double way left" : "Build double way right",
+		-2 * (sint64)d->get_price(), 2 * (sint32)d->get_maintenance() );
+	size_t n = strlen( toolstr );
+	sprintf( toolstr+n, ", %s, %dkm/h", d->get_name(), d->get_topspeed() );
+	return toolstr;
+}
+
+
+// The side has to reach the other clients. The base class deliberately reduces
+// the parameter to the way name, because the numeric form points at a default
+// that is not synchronised; do the same here but keep the side in front of it.
+const char *tool_build_way_double_t::get_default_param( player_t *player ) const
+{
+	if(  player == NULL  ||  side == double_way_builder_t::side_none  ) {
+		return default_param;
+	}
+	const way_desc_t *d = get_desc();
+	if(  d == NULL  ) {
+		return default_param;
+	}
+	sprintf( param_buf, "%c,%.240s", side == double_way_builder_t::side_left ? 'l' : 'r', d->get_name() );
+	return param_buf;
+}
+
+
+bool tool_build_way_double_t::is_selected() const
+{
+	tool_t const* const tool = welt->get_tool( welt->get_active_player_nr() );
+	if(  tool->get_id() != get_id()  ) {
+		return false;
+	}
+	// the left and the right button share the id and may share the way, so the
+	// side is what tells them apart
+	tool_build_way_double_t const* const selected = dynamic_cast<tool_build_way_double_t const*>(tool);
+	return selected  &&  selected->side == side  &&  selected->get_desc() == get_desc();
+}
+
+
+bool tool_build_way_double_t::init( player_t *player )
+{
+	// Only "<side>,<way>" counts. Reading the first character on its own would
+	// accept a truncated parameter like "l" and hand back a usable side for a
+	// tool that has no way at all, so demand the separator too.
+	side = double_way_builder_t::side_none;
+	if(  default_param  &&  default_param[0]  &&  default_param[1] == ','  ) {
+		switch(  default_param[0]  ) {
+			case 'l': case 'L': side = double_way_builder_t::side_left;  break;
+			case 'r': case 'R': side = double_way_builder_t::side_right; break;
+			default: break;
+		}
+	}
+	if(  !tool_build_way_t::init( player )  ) {
+		return false;
+	}
+	return side != double_way_builder_t::side_none  &&  double_way_builder_t::is_supported( get_desc() );
+}
+
+
+void tool_build_way_double_t::rdwr_custom_data( memory_rw_t *packet )
+{
+	two_click_tool_t::rdwr_custom_data( packet );
+	packet->rdwr_byte( side );
+}
+
+
+const char *tool_build_way_double_t::do_work( player_t *player, const koord3d &start, const koord3d &end )
+{
+	if(  end == koord3d::invalid  ||  player == NULL  ) {
+		return "";
+	}
+
+	way_builder_t main_way( player ), second_way( player );
+	const char *err = double_way_builder_t::plan( start, end,
+		(double_way_builder_t::side_t)side, get_desc(),
+		is_ctrl_pressed() ? double_way_builder_t::flag_straight : 0,
+		main_way, second_way );
+	if(  err  ) {
+		return err;
+	}
+
+	// Both routes were found on the world as it still is, and plan() has proven
+	// they are disjoint and free of bridges, tunnels and terraforming. From here
+	// only the money can stop us, so check it before anything is built.
+	const sint64 costs = main_way.calc_costs() + second_way.calc_costs();
+	if(  !player->can_afford( -costs )  ) {
+		return NOTICE_INSUFFICIENT_FUNDS;
+	}
+
+	welt->mute_sound(true);
+	main_way.build();
+	second_way.build();
+	welt->mute_sound(false);
+
+	return NULL;
+}
+
+
+void tool_build_way_double_t::mark_route( const koord3d_vector_t &route, player_t *player )
+{
+	for(  uint32 j = 0;  j < route.get_count();  j++  ) {
+		grund_t *gr = welt->lookup( route[j] );
+		if(  gr == NULL  ||  gr->is_water()  ) {
+			continue;
+		}
+		const ribi_t::ribi zeige = gr->get_weg_ribi_unmasked( desc->get_wtyp() ) | route.get_short_ribi( j );
+
+		zeiger_t *way = new zeiger_t( route[j], player );
+		if(  gr->get_weg_hang()  ) {
+			way->set_image( desc->get_slope_image_id( gr->get_weg_hang(), 0 ) );
+		}
+		else {
+			way->set_image( desc->get_image_id( zeige, 0 ) );
+		}
+		gr->obj_add( way );
+		way->set_yoff( -gr->get_weg_yoff() );
+		marked.insert( way );
+		way->mark_image_dirty( way->get_image(), 0 );
+	}
+}
+
+
+void tool_build_way_double_t::mark_tiles( player_t *player, const koord3d &start, const koord3d &end )
+{
+	way_builder_t main_way( player ), second_way( player );
+	const char *err = double_way_builder_t::plan( start, end,
+		(double_way_builder_t::side_t)side, get_desc(),
+		is_ctrl_pressed() ? double_way_builder_t::flag_straight : 0,
+		main_way, second_way );
+
+	if(  err == NULL  ) {
+		// same planner, same two routes and same sum that do_work() will use, so
+		// what is drawn here is exactly what gets built
+		const sint64 costs = main_way.calc_costs() + second_way.calc_costs();
+		if(  player != NULL  &&  !player->can_afford( -costs )  ) {
+			err = NOTICE_INSUFFICIENT_FUNDS;
+		}
+		else {
+			win_set_static_tooltip( tooltip_with_price_length( "Building costs estimates", costs,
+				main_way.get_count() + second_way.get_count() ) );
+			mark_route( main_way.get_route(), player );
+			mark_route( second_way.get_route(), player );
+			return;
+		}
+	}
+	// Nothing is drawn when the pair cannot be built. The preview can therefore
+	// never look buildable where do_work() would refuse it, which is the whole
+	// point of running the same plan() here.
+	win_set_static_tooltip( translator::translate( err[0] ? err : "Cannot build this here" ) );
+}
+
+
 /* bridge construction */
 const char* tool_build_bridge_t::get_tooltip(const player_t *) const
 {
diff --git a/src/simutrans/tool/simtool.h b/src/simutrans/tool/simtool.h
index 08e3dc40c..969f352a2 100644
--- a/src/simutrans/tool/simtool.h
+++ b/src/simutrans/tool/simtool.h
@@ -337,6 +337,59 @@ public:
 	waytype_t get_waytype() const OVERRIDE { return road_wt; }
 };
 
+/**
+ * Builds two parallel ways, one tile apart, from a single drag.
+ * All the geometry lives in double_way_builder_t; this class only carries the
+ * side the user asked for and turns a validated pair of routes into two builds.
+ *
+ * default_param is "<side>,<way name>", side being 'l' or 'r'. The side is also
+ * written into the tool's custom data so that the value chosen when the command
+ * was issued is the one every client replays, instead of whatever the tool
+ * happens to hold when the command arrives.
+ */
+class tool_build_way_double_t : public tool_build_way_t {
+private:
+	uint8 side; ///< double_way_builder_t::side_t
+
+	/// buffer for get_default_param(), which has to compose "<side>,<way name>"
+	mutable char param_buf[256];
+
+	/// storage for the parameter of the menu buttons, owned by this instance
+	char own_param[32];
+
+	char const* do_work(player_t*, koord3d const&, koord3d const&) OVERRIDE;
+	void mark_tiles(player_t*, koord3d const&, koord3d const&) OVERRIDE;
+
+	/// draws one planned route as preview cursors; adds nothing to the world itself
+	void mark_route(const koord3d_vector_t &route, player_t *player);
+
+public:
+	tool_build_way_double_t() : tool_build_way_t(TOOL_BUILD_WAY_DOUBLE | GENERAL_TOOL), side(0) { param_buf[0] = 0; own_param[0] = 0; }
+
+	/**
+	 * Points default_param at storage this tool owns, so the menu does not have
+	 * to keep a string alive for it. @p s is a double_way_builder_t::side_t.
+	 */
+	void set_menu_param(uint8 s, waytype_t wt);
+	way_desc_t const* get_desc() const OVERRIDE;
+	image_id get_icon(player_t*) const OVERRIDE;
+	char const* get_tooltip(player_t const*) const OVERRIDE;
+	char const* get_default_param(player_t*) const OVERRIDE;
+	bool is_selected() const OVERRIDE;
+	bool init(player_t*) OVERRIDE;
+	void rdwr_custom_data(memory_rw_t*) OVERRIDE;
+	bool is_init_keeps_game_state() const OVERRIDE { return true; }
+	uint8 get_side() const { return side; }
+
+	/* A scenario that forbids way building must not be circumvented by picking
+	 * the double variant, so this tool answers permission questions as if it
+	 * were the ordinary way tool. The per tile checks inside way_builder_t
+	 * already ask under TOOL_BUILD_WAY for both routes; this closes the earlier,
+	 * tool level question as well.
+	 */
+	uint16 get_scenario_tool_id() const OVERRIDE { return TOOL_BUILD_WAY | GENERAL_TOOL; }
+};
+
 class tool_build_bridge_t : public two_click_tool_t {
 private:
 	ribi_t::ribi ribi;
diff --git a/src/simutrans/world/simworld.cc b/src/simutrans/world/simworld.cc
index e1c354b44..523a78b68 100644
--- a/src/simutrans/world/simworld.cc
+++ b/src/simutrans/world/simworld.cc
@@ -2207,7 +2207,7 @@ void karte_t::set_tool_api( tool_t *tool_in, player_t *player, bool& suspended)
 	}
 	bool needs_check = !tool_in->no_check();
 	// check for scenario conditions
-	if(  needs_check  &&  !scenario->is_tool_allowed(player, tool_in->get_id(), tool_in->get_waytype(), tool_in->get_default_param())  ) {
+	if(  needs_check  &&  !scenario->is_tool_allowed(player, tool_in->get_scenario_tool_id(), tool_in->get_waytype(), tool_in->get_default_param())  ) {
 		return;
 	}
 	// check for password-protected players
@@ -5811,11 +5811,11 @@ const char* karte_t::call_work_api(tool_t *tool, player_t *player, koord3d pos,
 	tool->flags |= tool_t::WFL_LOCAL;
 	// first allowance by scenario
 	if(  (tool->flags & tool_t::WFL_NO_CHK) == 0  &&  get_scenario()->is_scripted()  ) {
-		if(  !get_scenario()->is_tool_allowed(player, tool->get_id(), tool->get_waytype(), tool->get_default_param())  ) {
+		if(  !get_scenario()->is_tool_allowed(player, tool->get_scenario_tool_id(), tool->get_waytype(), tool->get_default_param())  ) {
 			err = "Forbidden by scenario";
 		}
 		else {
-			err = get_scenario()->is_work_allowed_here(player, tool->get_id(), tool->get_waytype(), tool->get_default_param(), pos);
+			err = get_scenario()->is_work_allowed_here(player, tool->get_scenario_tool_id(), tool->get_waytype(), tool->get_default_param(), pos);
 		}
 	}
 	if (err == NULL) {
diff --git a/src/simutrans/builder/double_way_builder.h b/src/simutrans/builder/double_way_builder.h
new file mode 100644
index 000000000..c617b2052
--- /dev/null
+++ b/src/simutrans/builder/double_way_builder.h
@@ -0,0 +1,93 @@
+/*
+ * This file is part of the Simutrans project under the Artistic License.
+ * (see LICENSE.txt)
+ */
+
+#ifndef BUILDER_DOUBLE_WAY_BUILDER_H
+#define BUILDER_DOUBLE_WAY_BUILDER_H
+
+
+#include "../simtypes.h"
+#include "../dataobj/koord3d.h"
+
+
+class way_builder_t;
+class way_desc_t;
+class player_t;
+class karte_ptr_t;
+
+
+/**
+ * Plans two parallel ways, one tile apart, from a single pair of end points.
+ *
+ * The second route is not derived tile by tile. Only the end points are moved
+ * sideways and the ordinary way search computes the route between them, so
+ * is_allowed_step(), the cost model, the slope rules and the staircase
+ * (diagonal) handling are reused unchanged. What keeps that honest is
+ * check_corridor(), which refuses any second route that does not stay one tile
+ * beside the main one.
+ *
+ * Both routes are computed against the same untouched world: plan() never
+ * modifies anything, it only fills the two builders the caller passes in. The
+ * caller decides whether to pay for them and call build() on both.
+ *
+ * This is deliberately a small prototype. Bridges, tunnels and terraforming are
+ * not merely unsupported, they are structurally excluded: plan() passes no
+ * bridge or tunnel descriptor and never sets terraform_flag, which is what lets
+ * it promise that building the first way cannot invalidate the second.
+ */
+class double_way_builder_t
+{
+	static karte_ptr_t welt;
+
+public:
+	/// Which side the second way goes to, seen while travelling from start to end.
+	enum side_t {
+		side_none  = 0,
+		side_left  = 1,
+		side_right = 2
+	};
+
+	/// Subset of the way tool's modifiers this planner understands.
+	enum flag_t {
+		flag_straight = 1 << 0 ///< use the straight search, as CTRL does on the way tool
+	};
+
+	/**
+	 * Computes both routes on the current world state and checks that they form
+	 * a usable pair. Nothing is built and nothing is paid. The owner of the two
+	 * ways, and with it every permission check, comes from the two builders.
+	 *
+	 * @param start      tile the user clicked first
+	 * @param end        tile the user clicked last
+	 * @param side       which side of start->end the second way goes to
+	 * @param desc       way to build; the same one is used for both ways
+	 * @param flags      combination of flag_t, anything else is ignored
+	 * @param main_way   receives the route along start->end
+	 * @param second_way receives the parallel route
+	 *
+	 * @returns NULL when both routes may be built, otherwise an untranslated
+	 *          error message ("" means "refuse silently").
+	 */
+	static const char *plan(const koord3d &start, const koord3d &end,
+	                        side_t side, const way_desc_t *desc, uint8 flags,
+	                        way_builder_t &main_way, way_builder_t &second_way);
+
+	/// Waytypes this prototype accepts. Everything else keeps the single way tool.
+	static bool is_supported(const way_desc_t *desc);
+
+	/// Sideways offset of one tile for a way travelling in @p travel_dir.
+	static koord side_offset(ribi_t::ribi travel_dir, side_t side);
+
+private:
+	/**
+	 * Rejects a second route that is not a proper parallel of the main one:
+	 * shared tiles, gaps, a detour outside the one tile corridor, or end points
+	 * that do not match the requested side.
+	 * @returns NULL when the pair is acceptable, else an untranslated message.
+	 */
+	static const char *check_corridor(const way_builder_t &main_way, const way_builder_t &second_way,
+	                                  const koord3d &second_start, const koord3d &second_end);
+};
+
+#endif
diff --git a/src/simutrans/builder/double_way_builder.cc b/src/simutrans/builder/double_way_builder.cc
new file mode 100644
index 000000000..f49f14b81
--- /dev/null
+++ b/src/simutrans/builder/double_way_builder.cc
@@ -0,0 +1,276 @@
+/*
+ * This file is part of the Simutrans project under the Artistic License.
+ * (see LICENSE.txt)
+ */
+
+#include "double_way_builder.h"
+#include "wegbauer.h"
+
+#include "../world/simworld.h"
+#include "../ground/grund.h"
+#include "../descriptor/way_desc.h"
+#include "../dataobj/ribi.h"
+
+
+karte_ptr_t double_way_builder_t::welt;
+
+
+/// true when the two tiles touch, diagonals included
+static bool tiles_touch(const koord &a, const koord &b)
+{
+	return abs(a.x - b.x) <= 1  &&  abs(a.y - b.y) <= 1;
+}
+
+
+/// adds the map ground at @p pos to @p forbidden, if there is one
+static void forbid_ground(vector_tpl<koord3d> &forbidden, const koord &pos, const karte_t &world)
+{
+	if(  const grund_t *gr = world.lookup_kartenboden(pos)  ) {
+		forbidden.append_unique( gr->get_pos() );
+	}
+}
+
+
+/// true when @p pos touches any tile of @p route (a tile of the route counts too)
+static bool beside_route(const koord &pos, const koord3d_vector_t &route)
+{
+	for(  uint32 i = 0;  i < route.get_count();  i++  ) {
+		if(  tiles_touch(pos, route[i].get_2d())  ) {
+			return true;
+		}
+	}
+	return false;
+}
+
+
+koord double_way_builder_t::side_offset(ribi_t::ribi travel_dir, side_t side)
+{
+	// rotate90 turns clockwise on the map grid, so it yields the right hand side
+	// of a way travelling in travel_dir; rotate90l yields the left hand side.
+	return koord(side == side_right ? ribi_t::rotate90(travel_dir) : ribi_t::rotate90l(travel_dir));
+}
+
+
+bool double_way_builder_t::is_supported(const way_desc_t *desc)
+{
+	if(  desc == NULL  ) {
+		return false;
+	}
+	// elevated ways and runways use a different route search, rivers and canals a
+	// different geometry, powerlines are not ways at all. Keep the prototype on
+	// the waytypes the ordinary ground search handles.
+	if(  desc->get_styp() == type_elevated  ||  desc->get_styp() == type_river  ) {
+		return false;
+	}
+	switch(  desc->get_wtyp()  ) {
+		case road_wt:
+		case track_wt:
+		case tram_wt:
+		case narrowgauge_wt:
+		case monorail_wt:
+		case maglev_wt:
+			return true;
+		default:
+			return false;
+	}
+}
+
+
+const char *double_way_builder_t::plan(const koord3d &start, const koord3d &end,
+                                       side_t side, const way_desc_t *desc, uint8 flags,
+                                       way_builder_t &main_way, way_builder_t &second_way)
+{
+	if(  side != side_left  &&  side != side_right  ) {
+		return "";
+	}
+	if(  !is_supported(desc)  ) {
+		return "Double way building is not available for this way";
+	}
+	if(  start == end  ) {
+		return "";
+	}
+
+	way_builder_t::bautyp_t bautyp = (way_builder_t::bautyp_t)desc->get_wtyp();
+	if(  desc->is_tram()  ) {
+		bautyp = way_builder_t::schiene_tram;
+	}
+	// No terraform_flag and no bridge/tunnel descriptor, on purpose. Both builders
+	// therefore have bridge_desc == tunnel_desc == NULL, which makes
+	// build_tunnel_and_bridges() return immediately, and without terraform_flag
+	// build() never calls do_terraforming(). Together with is_supported(), which
+	// keeps elevated ways and runways out, that is what lets us promise that
+	// building the first way cannot change the ground under the second one.
+
+	// ---- main route, on the untouched world --------------------------------
+	main_way.init_builder(bautyp, desc, NULL, NULL);
+	main_way.set_keep_existing_faster_ways(true);
+
+	const char *err = (flags & flag_straight) ? main_way.calc_straight_route(start, end)
+	                                          : main_way.calc_route(start, end);
+	if(  main_way.get_count() < 2  ) {
+		return err ? err : "";
+	}
+
+	// The straight search fills the route from the start, the A* search fills it
+	// from the target backwards (wegbauer.cc), and nothing normalises that. Find
+	// out which end we clicked on instead of trusting the order, otherwise left
+	// and right would swap depending on which search ran.
+	const koord3d_vector_t &route = main_way.get_route();
+	const uint32 last = route.get_count() - 1;
+	const bool reversed = route[last].get_2d() == start.get_2d();
+	if(  !reversed  &&  route[0].get_2d() != start.get_2d()  ) {
+		// route does not touch the tile we started from: refuse rather than guess
+		return "";
+	}
+
+	const koord3d first_tile  = reversed ? route[last]   : route[0];
+	const koord3d after_first = reversed ? route[last-1] : route[1];
+	const koord3d last_tile   = reversed ? route[0]      : route[last];
+	const koord3d before_last = reversed ? route[1]      : route[last-1];
+
+	const koord zv_start = side_offset(ribi_type(first_tile, after_first), side);
+	const koord zv_end   = side_offset(ribi_type(before_last, last_tile), side);
+
+	// ---- displaced end points ----------------------------------------------
+	const grund_t *gr_start = welt->lookup_kartenboden(first_tile.get_2d() + zv_start);
+	const grund_t *gr_end   = welt->lookup_kartenboden(last_tile.get_2d() + zv_end);
+	if(  gr_start == NULL  ||  gr_end == NULL  ) {
+		// off the map
+		return "The second way would leave the map";
+	}
+	const koord3d second_start = gr_start->get_pos();
+	const koord3d second_end   = gr_end->get_pos();
+	if(  second_start == second_end  ) {
+		return "";
+	}
+
+	// ---- second route, still on the untouched world -------------------------
+	second_way.init_builder(bautyp, desc, NULL, NULL);
+	second_way.set_keep_existing_faster_ways(true);
+	// Confine the second search to the one tile corridor beside the first route.
+	// Nothing is built here: both routes are still computed against the world as
+	// it is now, the first route is only handed over as a set of tiles.
+	//
+	// Two things have to be forbidden, and forbidding only the first is not
+	// enough. The route itself, so the two ways cannot land on the same tile.
+	// And a fence two tiles out, because otherwise the search is merely pushed
+	// off the first route without being pulled along it: wherever two routes
+	// cost the same the search is free to bulge away around a bend. Since one
+	// step changes the distance to the first route by at most one, a tile can
+	// only get further than one tile away by standing on the fence first.
+	vector_tpl<koord3d> forbidden;
+	for(  uint32 i = 0;  i < route.get_count();  i++  ) {
+		forbidden.append_unique( route[i] );
+	}
+
+	// Cap both ends. The fence alone only keeps the second route within one tile
+	// of the first one, and that is not the same as keeping it on the chosen
+	// side: it can walk around an end of the first route and come back along the
+	// other side, which stays inside the corridor and satisfies every later
+	// check while being the wrong result entirely. With the route itself
+	// forbidden, the corridor fenced and both ends capped, a connected route
+	// that starts on one side can no longer reach the other.
+	const koord behind_start = -koord( ribi_type(first_tile, after_first) );
+	const koord beyond_end   =  koord( ribi_type(before_last, last_tile) );
+	const koord cap_start = first_tile.get_2d() + behind_start;
+	const koord cap_end   = last_tile.get_2d()  + beyond_end;
+	forbid_ground( forbidden, cap_start,            *welt );
+	forbid_ground( forbidden, cap_start + zv_start, *welt );
+	forbid_ground( forbidden, cap_start - zv_start, *welt );
+	forbid_ground( forbidden, cap_end,              *welt );
+	forbid_ground( forbidden, cap_end   + zv_end,   *welt );
+	forbid_ground( forbidden, cap_end   - zv_end,   *welt );
+	for(  uint32 i = 0;  i < route.get_count();  i++  ) {
+		for(  sint16 dx = -2;  dx <= 2;  dx++  ) {
+			for(  sint16 dy = -2;  dy <= 2;  dy++  ) {
+				if(  abs(dx) != 2  &&  abs(dy) != 2  ) {
+					continue; // inside the corridor, leave it open
+				}
+				const koord p = route[i].get_2d() + koord(dx, dy);
+				if(  beside_route( p, route )  ) {
+					continue; // some other part of the route is next to it
+				}
+				if(  const grund_t *gr = welt->lookup_kartenboden(p)  ) {
+					forbidden.append_unique( gr->get_pos() );
+				}
+			}
+		}
+	}
+	second_way.set_forbidden_tiles( forbidden );
+
+	const char *err2 = (flags & flag_straight) ? second_way.calc_straight_route(second_start, second_end)
+	                                           : second_way.calc_route(second_start, second_end);
+	if(  second_way.get_count() < 2  ) {
+		return err2 ? err2 : "The second way cannot be built here";
+	}
+
+	return check_corridor(main_way, second_way, second_start, second_end);
+}
+
+
+const char *double_way_builder_t::check_corridor(const way_builder_t &main_way, const way_builder_t &second_way,
+                                                 const koord3d &second_start, const koord3d &second_end)
+{
+	const koord3d_vector_t &a = main_way.get_route();
+	const koord3d_vector_t &b = second_way.get_route();
+
+	// end points must be the ones we asked for, in either order: the search may
+	// return the route from the target backwards
+	const koord3d b_front = b[0];
+	const koord3d b_back  = b[b.get_count()-1];
+	const bool ends_match = (b_front.get_2d() == second_start.get_2d()  &&  b_back.get_2d() == second_end.get_2d())
+	                     || (b_front.get_2d() == second_end.get_2d()    &&  b_back.get_2d() == second_start.get_2d());
+	if(  !ends_match  ) {
+		return "The second way does not start beside the first";
+	}
+
+	// Both routes must be continuous. Bridges and tunnels are already excluded by
+	// the NULL descriptors and by is_supported(); this is a second, independent
+	// net, because a gap in the route array is not by itself proof of what
+	// build() would do. Treat it as a consistency check, not as the guarantee.
+	for(  uint32 i = 1;  i < a.get_count();  i++  ) {
+		if(  koord_distance(a[i-1], a[i]) != 1  ) {
+			return "Double way building cannot use bridges or tunnels";
+		}
+	}
+	for(  uint32 i = 1;  i < b.get_count();  i++  ) {
+		if(  koord_distance(b[i-1], b[i]) != 1  ) {
+			return "Double way building cannot use bridges or tunnels";
+		}
+	}
+
+	// The two ways may never land on the same tile. plan() already forbids the
+	// first route to the second search, so this should be unreachable; it stays
+	// as the invariant that the corridor rests on.
+	for(  uint32 i = 0;  i < b.get_count();  i++  ) {
+		for(  uint32 j = 0;  j < a.get_count();  j++  ) {
+			if(  b[i].get_2d() == a[j].get_2d()  ) {
+				return "The two ways would cross each other";
+			}
+		}
+	}
+
+	// Every tile of one route has to touch a tile of the other. That is the
+	// corridor: it rejects a second route that wandered off somewhere valid but
+	// far away, and a main route left without a partner along part of its length.
+	for(  uint32 i = 0;  i < b.get_count();  i++  ) {
+		bool found = false;
+		for(  uint32 j = 0;  j < a.get_count()  &&  !found;  j++  ) {
+			found = tiles_touch(b[i].get_2d(), a[j].get_2d());
+		}
+		if(  !found  ) {
+			return "The second way would not stay beside the first";
+		}
+	}
+	for(  uint32 j = 0;  j < a.get_count();  j++  ) {
+		bool found = false;
+		for(  uint32 i = 0;  i < b.get_count()  &&  !found;  i++  ) {
+			found = tiles_touch(a[j].get_2d(), b[i].get_2d());
+		}
+		if(  !found  ) {
+			return "The second way would not stay beside the first";
+		}
+	}
+
+	return NULL;
+}
