diff --git a/simutrans/script/script_base.nut b/simutrans/script/script_base.nut
index 1483f70..59db956 100644
--- a/simutrans/script/script_base.nut
+++ b/simutrans/script/script_base.nut
@@ -481,6 +481,8 @@ class schedule_entry_x extends coord3d {
 	load = 0
 	/// waiting
 	wait = 0
+	/// if true, nothing is loaded at this stop (goods/passengers are only unloaded)
+	no_load = false
 }
 
 class dir {
diff --git a/src/simutrans/dataobj/schedule.cc b/src/simutrans/dataobj/schedule.cc
index acf4d87..5ea8fc8 100644
--- a/src/simutrans/dataobj/schedule.cc
+++ b/src/simutrans/dataobj/schedule.cc
@@ -341,6 +341,9 @@ void schedule_t::rdwr(loadsave_t *file)
 					file->rdwr_byte(wl);
 				}
 			}
+			if(  file->is_version_atleast(124, 6)  ) {
+				file->rdwr_bool(entries[i].no_load);
+			}
 		}
 	}
 	if(file->is_loading()) {
@@ -508,7 +511,7 @@ void schedule_t::sprintf_schedule( cbuffer_t &buf ) const
 {
 	buf.printf("%u|%d|", current_stop, (int)get_type());
 	for(schedule_entry_t const& i : entries) {
-		buf.printf("%s,%i,%i|", i.pos.get_str(), (int)i.minimum_loading, (int)i.waiting_time);
+		buf.printf("%s,%i,%i,%i|", i.pos.get_str(), (int)i.minimum_loading, (int)i.waiting_time, i.no_load ? 1 : 0);
 	}
 }
 
@@ -550,9 +553,12 @@ bool schedule_t::sscanf_schedule( const char *ptr )
 	}
 	p++;
 	// now scan the entries
+	// an entry has 5 values (pos.x,pos.y,pos.z,minimum_loading,waiting_time) and an
+	// optional 6th (no_load); older strings without it default no_load to off
 	while(  *p>0  ) {
-		sint16 values[5];
-		for(  sint8 i=0;  i<5;  i++  ) {
+		sint16 values[6];
+		values[5] = 0; // no_load flag: absent in old-format strings => off
+		for(  sint8 i=0;  i<6;  i++  ) {
 			values[i] = atoi( p );
 			while(  *p  &&  (*p!=','  &&  *p!='|')  ) {
 				p++;
@@ -561,14 +567,21 @@ bool schedule_t::sscanf_schedule( const char *ptr )
 				dbg->error( "schedule_t::sscanf_schedule()","incomplete string!" );
 				return false;
 			}
-			if(  i==4  &&  *p!='|'  ) {
+			if(  i>=4  &&  *p!=','  &&  *p!='|'  ) {
 				dbg->error( "schedule_t::sscanf_schedule()","incomplete entry termination!" );
 				return false;
 			}
+			const bool entry_end = (*p == '|');
 			p++;
+			if(  i>=4  &&  entry_end  ) {
+				// 5th value ended the entry (old format) or 6th value did (new format)
+				break;
+			}
 		}
 		// ok, now we have a complete entry
-		entries.append(schedule_entry_t(koord3d(values[0], values[1], (sint8)values[2]), (uint8)values[3], (uint16)values[4]));
+		schedule_entry_t new_entry(koord3d(values[0], values[1], (sint8)values[2]), (uint8)values[3], (uint16)values[4]);
+		new_entry.no_load = (values[5] != 0);
+		entries.append( new_entry );
 	}
 	make_valid();
 	return true;
diff --git a/src/simutrans/dataobj/schedule_entry.h b/src/simutrans/dataobj/schedule_entry.h
index 280ab63..09d3fab 100644
--- a/src/simutrans/dataobj/schedule_entry.h
+++ b/src/simutrans/dataobj/schedule_entry.h
@@ -17,12 +17,13 @@
 struct schedule_entry_t
 {
 public:
-	schedule_entry_t() {}
+	schedule_entry_t() : minimum_loading(0), waiting_time(0), no_load(false) {}
 
 	schedule_entry_t(koord3d const& pos, uint8 const minimum_loading, uint16 const waiting_time) :
 		pos(pos),
 		minimum_loading(minimum_loading),
-		waiting_time(waiting_time)
+		waiting_time(waiting_time),
+		no_load(false)
 	{}
 
 	/**
@@ -44,6 +45,12 @@ public:
 	 */
 	uint16 waiting_time;
 
+	/**
+	 * If true, the convoy does not load anything at this stop; it still unloads.
+	 * (ignored on waypoints)
+	 */
+	bool no_load;
+
 	uint32 get_waiting_ticks() const {
 		return  world()->ticks_per_world_month_shift >= 16  ? (uint32)waiting_time << (world()->ticks_per_world_month_shift - 16)
 		                                                    : (uint32)waiting_time >> (16 - world()->ticks_per_world_month_shift);
@@ -55,7 +62,7 @@ public:
 
 inline bool operator ==(const schedule_entry_t &a, const schedule_entry_t &b)
 {
-	return a.pos == b.pos  &&  a.minimum_loading == b.minimum_loading  &&  a.waiting_time == b.waiting_time;
+	return a.pos == b.pos  &&  a.minimum_loading == b.minimum_loading  &&  a.waiting_time == b.waiting_time  &&  a.no_load == b.no_load;
 }
 
 
diff --git a/src/simutrans/gui/components/gui_schedule.cc b/src/simutrans/gui/components/gui_schedule.cc
index 2b94a9c..7e0ceeb 100644
--- a/src/simutrans/gui/components/gui_schedule.cc
+++ b/src/simutrans/gui/components/gui_schedule.cc
@@ -422,6 +422,18 @@ gui_schedule_t::gui_schedule_t() :
 	}
 	end_table();
 
+	// per-stop "do not load here" toggle (unload only)
+	add_table( 2, 1 )->set_margin( scr_size(D_MARGIN_LEFT,0), scr_size(D_MARGIN_RIGHT,0) );
+	{
+		bt_no_load.init( button_t::square_state, "no load" );
+		bt_no_load.set_tooltip( "Do not load here; goods and passengers are only unloaded at this stop." );
+		bt_no_load.add_listener( this );
+		bt_no_load.set_rigid( true );
+		add_component( &bt_no_load );
+		new_component<gui_fill_t>();
+	}
+	end_table();
+
 	// action button row
 	button_row = add_table( 3, 1 );
 	button_row->set_margin( scr_size(D_MARGIN_LEFT,0), scr_size(D_MARGIN_RIGHT,0) );
@@ -555,6 +567,7 @@ void gui_schedule_t::update_selection()
 	departure.set_visible(false);
 	lb_load_str.set_visible(false);
 	lb_departure_str.set_visible(false);
+	bt_no_load.set_visible(false);
 
 	if(  !schedule->empty()  ) {
 		schedule->set_current_stop( min(schedule->get_count()-1,schedule->get_current_stop()) );
@@ -564,6 +577,8 @@ void gui_schedule_t::update_selection()
 		if(  haltestelle_t::get_halt(schedule->entries[current_stop].pos, player, schedule->get_waytype()==water_wt).is_bound()  ) {
 
 			cb_wait.set_visible(true);
+			bt_no_load.set_visible(true);
+			bt_no_load.pressed = schedule->entries[current_stop].no_load;
 			if( schedule->entries[current_stop].get_absolute_departures() ) {
 				cb_wait.set_selection( 1 );
 				lb_load_str.set_visible(true);
@@ -623,6 +638,13 @@ bool gui_schedule_t::action_triggered( gui_action_creator_t *comp, value_t p)
 		stats->update_schedule(true);
 		update_selection();
 	}
+	else if( comp == &bt_no_load) {
+		if(  !schedule->empty()  ) {
+			const uint8 stop = schedule->get_current_stop();
+			schedule->entries[stop].no_load = !schedule->entries[stop].no_load;
+			update_selection();
+		}
+	}
 	else if( comp == &cb_wait) {
 		if(  p.i==1  &&  schedule->entries[schedule->get_current_stop()].get_absolute_departures()==0  ) {
 			// absolute departure mode
diff --git a/src/simutrans/gui/components/gui_schedule.h b/src/simutrans/gui/components/gui_schedule.h
index e32868c..da3628e 100644
--- a/src/simutrans/gui/components/gui_schedule.h
+++ b/src/simutrans/gui/components/gui_schedule.h
@@ -42,6 +42,9 @@ private:
 	// always needed
 	button_t bt_revert, bt_return, bt_remove_double;
 
+	// per-stop "do not load here" toggle (only unload at this stop)
+	button_t bt_no_load;
+
 	gui_label_t lb_load_str, lb_departure_str;
 	gui_numberinput_t numimp_load;
 	gui_combobox_t cb_wait, insert_mode;
diff --git a/src/simutrans/script/api/api_schedule.cc b/src/simutrans/script/api/api_schedule.cc
index ffff11a..d353639 100644
--- a/src/simutrans/script/api/api_schedule.cc
+++ b/src/simutrans/script/api/api_schedule.cc
@@ -120,9 +120,15 @@ void append_entry(HSQUIRRELVM vm, SQInteger index, schedule_t* sched)
 	uint16 waiting_time_shift = 0;
 	get_slot(vm, "wait", waiting_time_shift, index);
 
+	bool no_load = false;
+	get_slot(vm, "no_load", no_load, index);
+
 	grund_t *gr = welt->lookup(pos);
 	if (gr) {
 		sched->append(gr, minimum_loading, waiting_time_shift);
+		if (no_load  &&  sched->get_count() > 0) {
+			sched->entries[sched->get_count() - 1].no_load = true;
+		}
 	}
 }
 
@@ -183,6 +189,10 @@ void export_schedule(HSQUIRRELVM vm)
 	 * Waiting time setting.
 	 */
 	integer wait;
+	/**
+	 * If true, nothing is loaded at this stop; goods and passengers are only unloaded.
+	 */
+	bool no_load;
 #endif
 
 	/**
diff --git a/src/simutrans/simconvoi.cc b/src/simutrans/simconvoi.cc
index ac79cdd..a220ceb 100644
--- a/src/simutrans/simconvoi.cc
+++ b/src/simutrans/simconvoi.cc
@@ -1876,6 +1876,23 @@ bool convoi_t::set_schedule(schedule_t * f)
 				unregister_stops();
 			}
 		}
+
+		// matches() compares the STOPS and nothing else - it says so itself, and it is
+		// right to, because that is the question a line asks. But "no load" is not a
+		// stop, and it is still part of the halt graph: a stop that takes nothing on
+		// board must not advertise the stops behind it (see rebuild_connections()).
+		//
+		// So ticking the box on an otherwise untouched schedule changes no stop, sets
+		// changed=false, never bumps the schedule counter, and the connections are
+		// never rebuilt - the convoy quietly stops loading while the graph keeps
+		// routing goods to it. The stops do not need re-registering; the graph does.
+		bool loading_changed = false;
+		if(  !changed  &&  schedule  ) {
+			loading_changed = schedule->get_count() != f->get_count();
+			for(  uint8 i = 0;  !loading_changed  &&  i < f->get_count();  i++  ) {
+				loading_changed = schedule->entries[i].no_load != f->entries[i].no_load;
+			}
+		}
 #ifdef BAD_IDEA
 		// destroy a possibly open schedule window
 		if(  schedule  &&  !schedule->is_editing_finished()  ) {
@@ -1890,6 +1907,10 @@ bool convoi_t::set_schedule(schedule_t * f)
 			register_stops();
 			welt->set_schedule_counter(); // must trigger refresh
 		}
+		else if(  loading_changed  ) {
+			// same stops, but what they load has changed: the graph must be rebuilt
+			welt->set_schedule_counter();
+		}
 	}
 
 	// remove wrong freight
@@ -2979,8 +3000,9 @@ void convoi_t::hat_gehalten(halthandle_t halt)
 	bool all_served_this_stop = true;
 
 	// prepare a list of all destination halts in the schedule
+	// (skipped when this schedule entry forbids loading: unload only, pick up nothing)
 	vector_tpl<halthandle_t> destination_halts(schedule->get_count());
-	if (!no_load) {
+	if (!no_load  &&  !schedule->get_current_entry().no_load) {
 		const uint8 count = schedule->get_count();
 		bool first_entry = true;
 		for (uint8 i = 1; i < count; i++) {
@@ -3090,7 +3112,8 @@ void convoi_t::hat_gehalten(halthandle_t halt)
 		// nothing to unload or load: waiting for cargo
 		unloading_state = false;
 	}
-	loading_limit = schedule->get_current_entry().minimum_loading;
+	// a "no load" stop never waits for a minimum load (nothing is picked up here)
+	loading_limit = schedule->get_current_entry().no_load ? 0 : schedule->get_current_entry().minimum_loading;
 
 	if(!no_load  ||  wants_more) {
 		// only check for further waiting, in not no_load is set
diff --git a/src/simutrans/simhalt.cc b/src/simutrans/simhalt.cc
index 93f9ac9..abd4275 100644
--- a/src/simutrans/simhalt.cc
+++ b/src/simutrans/simhalt.cc
@@ -1366,6 +1366,17 @@ sint32 haltestelle_t::rebuild_connections()
 		}
 		++start_index; // the next index after self halt; it's okay to be out-of-range
 
+		// Does this service take anything on board HERE? An entry flagged "no load"
+		// unloads and drives on, so the halts behind it are not reachable from here by
+		// this service, and we must not advertise them: goods would happily route to
+		// this halt to board a convoy that is never going to pick them up, and pile up
+		// waiting for a ride that does not exist.
+		//
+		// The flag belongs to the VISIT, not to the halt - one schedule may call here
+		// twice and load only on the second call - so it is re-read further down every
+		// time the walk comes back round to self.
+		bool loads_here = start_index > schedule->get_count()  ||  !schedule->entries[start_index - 1].no_load;
+
 		// determine goods category indices supported by this halt
 		supported_catg_index.clear();
 		for(uint8 const catg_index : *goods_catg_index) {
@@ -1387,7 +1398,8 @@ sint32 haltestelle_t::rebuild_connections()
 		uint16 aggregate_weight = WEIGHT_WAIT;
 		for(  uint8 j=0;  j<schedule->get_count();  ++j  ) {
 
-			halthandle_t current_halt = get_halt(schedule->entries[(start_index+j)%schedule->get_count()].pos, owner, schedule->get_waytype()==water_wt );
+			const uint8 current_index = (start_index+j)%schedule->get_count();
+			halthandle_t current_halt = get_halt(schedule->entries[current_index].pos, owner, schedule->get_waytype()==water_wt );
 			if(  !current_halt.is_bound()  ) {
 				// ignore way points
 				continue;
@@ -1403,6 +1415,8 @@ sint32 haltestelle_t::rebuild_connections()
 				}
 				// reset aggregate weight
 				aggregate_weight = WEIGHT_WAIT;
+				// the service calls here again: whether it loads is this entry's business
+				loads_here = !schedule->entries[current_index].no_load;
 				continue;
 			}
 
@@ -1418,9 +1432,12 @@ sint32 haltestelle_t::rebuild_connections()
 					previous_halt[catg_index] = current_halt;
 
 					// either add a new connection or update the weight of an existing connection where necessary
-					connection_t *const existing_connection = all_links[catg_index].connections.insert_unique_ordered( connection_t( current_halt, aggregate_weight ), connection_t::compare );
-					if(  existing_connection  &&  aggregate_weight<existing_connection->weight  ) {
-						existing_connection->weight = aggregate_weight;
+					// - but only if this service actually loads here (see loads_here above)
+					if(  loads_here  ) {
+						connection_t *const existing_connection = all_links[catg_index].connections.insert_unique_ordered( connection_t( current_halt, aggregate_weight ), connection_t::compare );
+						if(  existing_connection  &&  aggregate_weight<existing_connection->weight  ) {
+							existing_connection->weight = aggregate_weight;
+						}
 					}
 				}
 			}
diff --git a/src/simutrans/simversion.h b/src/simutrans/simversion.h
index 92eaa67..5ba5d23 100644
--- a/src/simutrans/simversion.h
+++ b/src/simutrans/simversion.h
@@ -22,7 +22,7 @@
 
 // Beware: SAVEGAME minor is often ahead of version minor when there were patches.
 // ==> These have no direct connection at all!
-#define SIM_SAVE_MINOR      5
+#define SIM_SAVE_MINOR      6
 #define SIM_SERVER_MINOR    5
 // NOTE: increment before next release to enable save/load of new features
 
diff --git a/tests/all_tests.nut b/tests/all_tests.nut
index 301e39b..c09bef3 100644
--- a/tests/all_tests.nut
+++ b/tests/all_tests.nut
@@ -184,6 +184,8 @@ all_tests <- [
 	test_transport_pax_valid_route,
 	test_transport_mail_valid_route,
 	test_transport_freight_valid_route,
+	test_transport_freight_no_load,
+	test_transport_freight_no_load_strands_waiting_freight,
 	test_trees_plant_single_invalid_pos,
 	test_trees_plant_single_invalid_param,
 	test_trees_plant_single_null_param,
diff --git a/tests/tests/test_transport.nut b/tests/tests/test_transport.nut
index 63f115d..bde1820 100644
--- a/tests/tests/test_transport.nut
+++ b/tests/tests/test_transport.nut
@@ -715,3 +715,211 @@ function test_transport_freight_valid_route()
 	ASSERT_EQUAL(command_x(tool_remove_way).work(player_x(0), coord3d(5, 7, 0), coord3d(4, 7, 0), "" + wt_road), null)
 	RESET_ALL_PLAYER_FUNDS()
 }
+
+
+function test_transport_freight_no_load_strands_waiting_freight()
+{
+	// WHAT HAPPENS TO THE FREIGHT THAT IS ALREADY STANDING THERE?
+	//
+	// You tick "no load" on the only service that calls at a stop, and eighteen tonnes of
+	// coal are already waiting on the platform with a route in their pocket. That route
+	// has just stopped existing.
+	//
+	// The engine does not leave them stranded and it does not deliver them: after the
+	// connections are rebuilt, reroute_goods() re-runs the search for every waiting ware
+	// and DISCARDS the ones that no longer have any route at all (simhalt.cc), while
+	// un-booking them from the factory's in-transit count. This is not new behaviour and
+	// it is not special to this flag - it is exactly what already happens when you delete
+	// the last line serving a stop. This test pins it down for the flag.
+	//
+	// Freight with an ALTERNATIVE route is not discarded; it is simply re-routed onto it.
+	local pl = player_x(0)
+
+	ASSERT_EQUAL(command_x(tool_build_way).work(pl, coord3d(4, 2, 0), coord3d(4, 8, 0), "cobblestone_road"), null)
+	ASSERT_EQUAL(command_x(tool_build_way).work(pl, coord3d(4, 7, 0), coord3d(5, 7, 0), "cobblestone_road"), null)
+	ASSERT_EQUAL(command_x(tool_build_station).work(pl, coord3d(4, 2, 0), "CarStop"), null)
+	ASSERT_EQUAL(command_x(tool_build_station).work(pl, coord3d(5, 7, 0), "CarStop"), null)
+	ASSERT_EQUAL(command_x.build_depot(pl, coord3d(4, 8, 0), building_desc_x("CarDepot")), null)
+
+	local depot = depot_x(4, 8, 0)
+	local from_halt = halt_x.get_halt(coord3d(5, 7, 0), pl)
+	local to_halt   = halt_x.get_halt(coord3d(4, 2, 0), pl)
+
+	depot.append_vehicle(pl, convoy_x(0), vehicle_desc_x("Kohletransporter"))
+	local cnv = depot.get_convoy_list()[0]
+
+	// an ordinary schedule: both stops load. The convoy stays IN THE DEPOT, so nothing can
+	// be picked up while we set this up - the freight just sits there with a valid route.
+	cnv.change_schedule(pl, schedule_x(wt_road, [ schedule_entry_x(coord3d(5, 7, 0), 0, 0),
+	                                              schedule_entry_x(coord3d(4, 2, 0), 0, 0) ]))
+
+	while(!halt_x.is_rerouting_finished()) {
+		sleep()
+	}
+
+	ASSERT_EQUAL(world.generate_goods(coord(3, 7), coord(3, 2), good_desc_x("Kohle"), 18), 1) // 1 == OK
+	sleep()
+	sleep()
+
+	// there they are, waiting, with a route
+	ASSERT_EQUAL(from_halt.waiting[0], 18)
+	ASSERT_EQUAL(from_halt.get_freight_to_halt(good_desc_x("Kohle"), to_halt), 18)
+
+	// now the player ticks the box on the stop they are standing on
+	local pickup = schedule_entry_x(coord3d(5, 7, 0), 0, 0)
+	pickup.no_load = true
+	cnv.change_schedule(pl, schedule_x(wt_road, [ pickup, schedule_entry_x(coord3d(4, 2, 0), 0, 0) ]))
+
+	while(!halt_x.is_rerouting_finished()) {
+		sleep()
+	}
+	sleep()
+	sleep()
+
+	// the route they were holding no longer exists and there is no other, so they are gone:
+	// not delivered, not stranded on the platform for ever, and not left on the factory's
+	// books as "in transit".
+	ASSERT_EQUAL(from_halt.waiting[0], 0)
+	ASSERT_EQUAL(from_halt.get_freight_to_halt(good_desc_x("Kohle"), to_halt), 0)
+	ASSERT_EQUAL(from_halt.departed[0], 0)
+	ASSERT_EQUAL(to_halt.arrived[0], 0)
+
+	// And they go QUIETLY: they are not booked as "no route" either. reroute_goods()
+	// drops them from the array without touching the halt's statistics, so from the
+	// player's side the coal simply stops being there. That is the engine's existing
+	// behaviour for goods left without a route - deleting the last line serving a stop
+	// does exactly the same - and this assertion is here to say so out loud rather than
+	// to bless it.
+	ASSERT_EQUAL(from_halt.noroute[0], 0)
+
+	// clean up
+	cnv.destroy(pl)
+	sleep()
+	sleep()
+	ASSERT_EQUAL(command_x(tool_remover).work(player_x(0), coord3d(4, 2, 0)), null)
+	ASSERT_EQUAL(command_x(tool_remover).work(player_x(0), coord3d(5, 7, 0)), null)
+	ASSERT_EQUAL(command_x(tool_remover).work(player_x(0), coord3d(4, 8, 0)), null)
+	ASSERT_EQUAL(command_x(tool_remove_way).work(player_x(0), coord3d(4, 2, 0), coord3d(4, 8, 0), "" + wt_road), null)
+	ASSERT_EQUAL(command_x(tool_remove_way).work(player_x(0), coord3d(5, 7, 0), coord3d(4, 7, 0), "" + wt_road), null)
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+function test_transport_freight_no_load()
+{
+	local pl = player_x(0)
+
+	ASSERT_EQUAL(command_x(tool_build_way).work(pl, coord3d(4, 2, 0), coord3d(4, 8, 0), "cobblestone_road"), null)
+	ASSERT_EQUAL(command_x(tool_build_way).work(pl, coord3d(4, 7, 0), coord3d(5, 7, 0), "cobblestone_road"), null)
+	ASSERT_EQUAL(command_x(tool_build_station).work(pl, coord3d(4, 2, 0), "CarStop"), null)
+	ASSERT_EQUAL(command_x(tool_build_station).work(pl, coord3d(5, 7, 0), "CarStop"), null)
+	ASSERT_EQUAL(command_x.build_depot(pl, coord3d(4, 8, 0), building_desc_x("CarDepot")), null)
+
+	local depot = depot_x(4, 8, 0)
+	local from_halt = halt_x.get_halt(coord3d(5, 7, 0), pl)
+	local to_halt   = halt_x.get_halt(coord3d(4, 2, 0), pl)
+
+	// create vehicle ...
+	depot.append_vehicle(pl, convoy_x(0), vehicle_desc_x("Kohletransporter"))
+	local cnv = depot.get_convoy_list()[0]
+
+	// ... with a 2-entry schedule where the pickup stop (5,7) is flagged "no load" ...
+	local pickup = schedule_entry_x(coord3d(5, 7, 0), 0, 0)
+	pickup.no_load = true
+	local sched = schedule_x(wt_road, [ pickup, schedule_entry_x(coord3d(4, 2, 0), 0, 0) ])
+	cnv.change_schedule(pl, sched)
+
+	// and start the convoy
+	depot.start_all_convoys(pl)
+
+	// make sure that the graph linking and all halts is updated
+	while(!halt_x.is_rerouting_finished()) {
+		sleep()
+	}
+
+	// (1) A STOP THAT LOADS NOTHING IS NOT A DEPARTURE POINT.
+	//
+	// It is not enough that the convoy declines to load: the halt must not ADVERTISE
+	// the stops behind it either. Otherwise the goods still see a route, walk to this
+	// halt, and pile up waiting for a ride that is never going to take them - which is
+	// worse than having no service at all, because it also blocks the halt.
+	//
+	// So there is no route from here, and 0 is what "no route" looks like.
+	ASSERT_EQUAL(world.generate_goods(coord(3, 7), coord(3, 2), good_desc_x("Kohle"), 18), 0)
+
+	sleep()
+	sleep()
+
+	ASSERT_EQUAL(from_halt.waiting[0], 0)
+	ASSERT_EQUAL(from_halt.get_freight_to_halt(good_desc_x("Kohle"), to_halt), 0)
+
+	// wait until the convoy has served the "no load" halt at least once
+	while (from_halt.convoys[0] == 0) {
+		sleep()
+	}
+
+	// give it ample time to (not) load and to loop around
+	for (local i = 0; i < 20; i++) {
+		sleep()
+	}
+
+	ASSERT_EQUAL(from_halt.waiting[0], 0)
+	ASSERT_EQUAL(from_halt.departed[0], 0)
+	ASSERT_EQUAL(to_halt.arrived[0], 0)
+
+	// (2) BUT IT STILL UNLOADS - that is the whole point of "unload only".
+	//
+	// Same coal, the other way round: it boards at the stop that does load, rides, and
+	// must be DELIVERED here. The stop is still a destination; it is only not a source.
+	ASSERT_EQUAL(world.generate_goods(coord(3, 2), coord(3, 7), good_desc_x("Kohle"), 18), 1) // 1 == OK
+
+	sleep()
+	sleep()
+
+	ASSERT_EQUAL(to_halt.waiting[0], 18)
+	ASSERT_EQUAL(to_halt.get_freight_to_halt(good_desc_x("Kohle"), from_halt), 18)
+
+	// make sure unloading has finished
+	while (from_halt.arrived[0] < 18) {
+		sleep()
+	}
+
+	ASSERT_EQUAL(to_halt.departed[0], 18)
+	ASSERT_EQUAL(from_halt.arrived[0], 18)
+	ASSERT_EQUAL(from_halt.departed[0], 0)      // still never loads
+
+	// (3) AND CLEARING THE FLAG MUST BRING THE STOP BACK.
+	//
+	// This is the half that is easy to miss. The schedule keeps exactly the same stops,
+	// so the usual "has the schedule changed?" test - which compares stops and says so
+	// itself - sees no change at all, and nothing tells the halt graph to rebuild. The
+	// convoy would start loading again while the graph still refused to route anything
+	// here, which is the same bug wearing the opposite mask.
+	local reload = schedule_x(wt_road, [ schedule_entry_x(coord3d(5, 7, 0), 0, 0),
+	                                     schedule_entry_x(coord3d(4, 2, 0), 0, 0) ])
+	cnv.change_schedule(pl, reload)
+
+	while(!halt_x.is_rerouting_finished()) {
+		sleep()
+	}
+
+	// the stop is a departure point again: there is a route, and the coal leaves
+	ASSERT_EQUAL(world.generate_goods(coord(3, 7), coord(3, 2), good_desc_x("Kohle"), 18), 1) // 1 == OK
+
+	while (from_halt.departed[0] < 18) {
+		sleep()
+	}
+
+	ASSERT_EQUAL(from_halt.departed[0], 18)
+
+	// clean up
+	cnv.destroy(pl)
+	sleep()
+	sleep() // make sure the convoy is destroyed
+	ASSERT_EQUAL(command_x(tool_remover).work(player_x(0), coord3d(4, 2, 0)), null)
+	ASSERT_EQUAL(command_x(tool_remover).work(player_x(0), coord3d(5, 7, 0)), null)
+	ASSERT_EQUAL(command_x(tool_remover).work(player_x(0), coord3d(4, 8, 0)), null)
+	ASSERT_EQUAL(command_x(tool_remove_way).work(player_x(0), coord3d(4, 2, 0), coord3d(4, 8, 0), "" + wt_road), null)
+	ASSERT_EQUAL(command_x(tool_remove_way).work(player_x(0), coord3d(5, 7, 0), coord3d(4, 7, 0), "" + wt_road), null)
+	RESET_ALL_PLAYER_FUNDS()
+}
