Skip to content

feature : radius to find stations around coordinates#1360

Merged
felixguendling merged 7 commits into
motis-project:masterfrom
orhazal:add-radius
Apr 3, 2026
Merged

feature : radius to find stations around coordinates#1360
felixguendling merged 7 commits into
motis-project:masterfrom
orhazal:add-radius

Conversation

@orhazal

@orhazal orhazal commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

For a Motis instance containing only GTFS data (without any OSM/street routing data), calling the /plan API with coordinates does not work as Motis cannot compute routes between coordinates and stations

To enable station-to-station itinerary planning in this setup, a radius-based approach is needed. When a radius is provided, all stops within that radius are retrieved from the location RTree and used as zero-duration offsets. This allows computing pure train journeys using coordinates without requiring OSM data

Radius based offsets are only applied when the radius parameter is explicitly set in the /plan query

Note : i have little to no experience with C++, but i'm very enthusiastic about this project and happy to learn and contribute

Follow-up : https://matrix.to/#/!kDMWQgLgnsbvxbEJdH:matrix.org/$vShE6andX9uV8oqTtdDxuBdHdxWbCPOp6n3MxmV3VtQ?via=matrix.org&via=kde.org&via=matrix.tu-berlin.de

@felixguendling

Copy link
Copy Markdown
Member

What is the reason to not just load OSM or let the user choose a stop nearby via geocoding?

With this implementation, you can get results that require you to walk a long detour to cross a river, motorway or train tracks which is less user friendly than just letting the user select a stop.

@orhazal

orhazal commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

This addresses a specific need for SNCF itinerary planning. One of the legacy planning modes is not door-to-door, but station-to-station. It is still used in particular for train ticket sales.

In this context, the risk of detours is largely acceptable, since this mode is not intended for door-to-door usage.

The goal is simply to identify nearby stations within a given radius and compute rail-only journeys between them, rather than provide accurate pedestrian routing

If needed by the user, pre-routing and post-routing are currently handled separately through other mechanisms

@orhazal

orhazal commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

I also tried including the France OSM data following your suggestion in the matrix discussion, with the idea of filtering out pre-routing and post-routing segments on our side if we don’t need them.

However, I noticed that the /plan endpoint became significantly slower when using the following parameters:
preTransitModes=WALK,CAR
postTransitModes=WALK,CAR
maxPreTransitTime=3600
maxPostTransitTime=3600

This seems to introduce a noticeable performance overhead compared to the station-to-station approach

@felixguendling

Copy link
Copy Markdown
Member

We should definitely have a test case for this functionality then. Otherwise, a future change might introduce bugs without us noticing. I think you can just extend an existing test case like the routing test.

Comment thread src/endpoints/routing.cc Outdated
auto prepare_stats = std::map<std::string, std::uint64_t>{};

auto const get_radius_offsets = [&](place_t const& p) {
if (!query.radius_.has_value() || loc_tree_ == nullptr) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loc_rtree_ should be set exactly when tt_ != nullptr

so we can just verify this here tt_ != nullptr && tags_ != nullptr <- add loc_rtree_ != nullptr

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and in this case, dest_has_radius and start_has_radius become both query.radius_.has_value() (can be inlined)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, i've inlined as asked. i also removed the has radius check from the get_radius_offsets lambda as it is checked when assigning start_ and end_ (lines 873 and 884)

@orhazal orhazal Apr 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it broke the routing when only one of the origin or destination is a coordinate. i've added a commit to fix this + a test

Comment thread openapi.yaml Outdated
orhazal and others added 3 commits April 3, 2026 00:37
Comment thread openapi.yaml Outdated
Comment thread test/routing_test.cc Outdated
"&timetableView=false");
ASSERT_FALSE(res.itineraries_.empty());
EXPECT_TRUE(std::any_of(
res.itineraries_.front().legs_.begin(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use utl::any_of (and other helpers) that don't require begin+end if you want to apply it to the whole range

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done !

Comment thread src/endpoints/routing.cc Outdated
auto prepare_stats = std::map<std::string, std::uint64_t>{};

auto const use_radius_start = query.radius_.has_value() &&
std::get_if<osr::location>(&start) != nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use std::holds_alternative

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done !

@felixguendling

felixguendling commented Apr 3, 2026

Copy link
Copy Markdown
Member

The way it's programmed now, you will only get the fastest option from all stops regardless of how far away they are. So if A is 1m away and B is 10km away but the option from B is 1min faster, you will never see the option from A (even though the journey via A is probably more interesting). Alternatives from different stops dominate each other directly. Another option would be to apply an offset based on beeline distance multiplied with an average speed.

Co-authored-by: Felix Gündling <felix.guendling@gmail.com>
@traines-source

Copy link
Copy Markdown
Contributor

There might be one other option to address this use case: Meta stations, i.e. amend the feed with a special stop_group_elements.txt to group multiple stops of a city to a meta stop. We intend to do this for e.g. Paris over at public-transport/transitous#1976.

This would require curation of stop groups outside of MOTIS, enabling more fine-granular control over which stops are considered, which might make it faster (e.g. otherwise with the radius in Paris hundreds or thousands of bus stops will be considered). I'm not sure what exactly is the main factor that makes the CAR pre/postTransit solution you proposed slower in your case – it probably also depends on the local circumstances in the queried area. There will always be the one-to-all CAR routing that takes some time where we might have room for improvement, then the matching to the stops (which should be negligible if you had preprocess_max_matching_distance enabled – default in newer MOTIS versions) and then starting from thousands of stops with possibly many more departures of course also takes more time (but that would be the case with the radius approach as well).

@felixguendling

Copy link
Copy Markdown
Member

@orhazal maybe you can explain a bit more how your current solution works? If it has the same shortcomings I described and you just want to replicate it "bug-by-bug", then this PR might be one option.

However, if you for example have a faster car routing outside of MOTIS such as OSRM, it might be an option to supply the offsets computed by OSRM to MOTIS. This would eliminate the disadvantage of the approach in this PR.

@orhazal

orhazal commented Apr 3, 2026

Copy link
Copy Markdown
Contributor Author

@traines-source
Thanks for the context !
In our case, we're loading a train-only timetable (SNCF rail) with no urban public transport, so the station density is very low, a 5-10km radius would typically match just 1-3 stations at most. We're also using stop_group_elements.txt in our MOTIS instance, so the two approaches complement each other nicely. The radius is useful for cases where coordinates are provided without any stop info.

@felixguendling
Our current solution has two routing modes : one that do exclusive train-to-train routing without any consideration for pre/post routings : it is used for train ticket sales. The second mode is door-to-door but prioritize long-distance train journeys, then construct pre/post routings around them as needed (using specific business rules).
For the first routing mode : the radius is good as-is. For the second one, the beeline distance offset would be a nice-to-have and i would love to work on it soon.

This PR provides this legacy functionality we need for SNCF Connect. However, in the long term, I personally hope we can move toward a single, unified itinerary engine.

Comment thread openapi.yaml Outdated
@felixguendling

Copy link
Copy Markdown
Member

In case the radius overlaps, you won't find any connections with the current approach.

If we switch to offsets fastestDirectFactor can control how direct connections (the sum of two offsets) will dominate public transport connections.

@orhazal

orhazal commented Apr 3, 2026

Copy link
Copy Markdown
Contributor Author

In our use case, we have a business rule that only activates the radius from 100 km. This is due to the “segmented” computation of journeys coming from multiple sources. For short distances, the rules are completely different from those used for train/long-distance travel.

As you mentioned earlier, this is within a legacy context with case-by-case or bug-by-bug replication.
And again, I hope that one day we’ll move to a single routing engine.

@felixguendling For this PR, do we need to add protection against radius overlap?

@felixguendling

Copy link
Copy Markdown
Member

For this PR, do we need to add protection against radius overlap?

I think we can leave it like it is now. Garbage in = garbage out.

@felixguendling
felixguendling merged commit 41d9c6f into motis-project:master Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants