1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
//! An implementation of [RAPTOR][paper] (Delling, Pajor, Werneck): given a
//! public transit network, find all Pareto-optimal journeys between two stops,
//! trading fewer transfers against earlier arrival.
//!
//! [paper]: https://www.microsoft.com/en-us/research/publication/round-based-public-transit-routing/
//!
//! # Quick start
//!
//! Most users start with the bundled [`gtfs`] adapter, which wraps a parsed
//! GTFS feed and implements [`Timetable`] for it.
//!
//! ```no_run
//! use gtfs_structures::Gtfs;
//! use jiff::civil::date;
//! use vulture::{SecondOfDay, Timetable};
//! use vulture::gtfs::GtfsTimetable;
//!
//! # fn main() -> anyhow::Result<()> {
//! let gtfs = Gtfs::new("path/to/gtfs.zip")?;
//! // Pin the timetable to one service date; inactive trips are filtered out.
//! let tt = GtfsTimetable::new(>fs, date(2026, 5, 4))?;
//!
//! // The algorithm takes dense u32 indices, not GTFS string IDs – resolve first.
//! let start = tt.stop_idx("dilshad_garden").expect("unknown stop");
//! let target = tt.stop_idx("vishwavidyalaya").expect("unknown stop");
//!
//! let journeys = tt
//! .query()
//! .from(start)
//! .to(target)
//! .max_transfers(10)
//! .depart_at(SecondOfDay::hms(9, 0, 0))
//! .run();
//!
//! for j in &journeys {
//! println!("{} trip(s), arrives {}", j.plan.len(), j.arrival());
//! }
//! # Ok(())
//! # }
//! ```
//!
//! # Concepts
//!
//! RAPTOR proceeds in *rounds*. Round k holds the earliest known arrival time
//! at every stop reachable using at most k trips. Each round scans the routes
//! through stops improved in the previous round and then relaxes walking
//! footpaths to a fixed point. The output is a Pareto front: one [`Journey`]
//! per trip count between zero and `max_transfers`, with strictly earlier
//! arrivals as you allow more trips. There is no Dijkstra-style priority queue
//! and no shortest-path tree – just successive rounds of array updates.
//!
//! The default routing is *single-criterion*: minimise arrival time, then
//! report one journey per trip count. The criterion under optimisation is
//! abstracted by the [`Label`] trait; the default [`ArrivalTime`] just carries
//! a [`SecondOfDay`]. Multi-criterion routing (e.g. trade-offs between arrival
//! time and accumulated walking time) plugs in via [`Timetable::query_with_label`]
//! with one of the [`labels`] impls or your own.
//!
//! # Common patterns
//!
//! - **Single query.** [`Timetable::query`] returns a typestate builder.
//! Chain `.from(...).to(...).max_transfers(...).depart_at(...).run()`.
//! - **Multi-platform stations.** [`gtfs::GtfsTimetable::station_stops`]
//! expands a parent station to its child platforms; pass it to `.from(...)` /
//! `.to(...)`.
//! - **Range queries.** [`Query::depart_in_window`] returns a
//! [`Vec<RangeJourney>`](RangeJourney) Pareto profile across departure times.
//! Serial `.run()` is rRAPTOR; `.run_par()` / `.run_with_pool(&pool)` fan
//! per-departure work across Rayon (default-on `parallel` feature).
//! - **Server reuse.** Allocate a [`RaptorCache`] once via
//! [`RaptorCache::for_timetable`] and finish each chain with
//! `.run_with_cache(&mut cache)` – amortises scratch-buffer allocation
//! across queries.
//! - **Multi-threaded reuse.** [`RaptorCachePool`] is the `Sync` variant.
//! Each worker calls [`RaptorCachePool::checkout`] to get a cache for one
//! query; the cache returns to the pool on drop.
//! - **Multi-criterion routing.** [`Timetable::query_with_label`] takes a
//! custom [`Label`]. The bundled [`labels::ArrivalAndWalk`] trades arrival
//! time against accumulated walking time; [`labels::ArrivalAndFare`]
//! trades arrival time against accumulated fare from a route → fare
//! table supplied via [`Query::with_context`].
//! - **Per-leg trip IDs and timing.** [`Journey::with_timing`] reconstructs
//! per-leg `(boarding stop, trip, depart, alight stop, arrive)` tuples.
//! `Journey.plan` on its own is just topology.
//! - **Wheelchair-accessibility filter.** Chain
//! [`Query::require_wheelchair_accessible`] on the builder to skip
//! trips and stops marked `NotAvailable` in the GTFS
//! `wheelchair_accessible` / `wheelchair_boarding` fields. Default
//! behaviour is unchanged.
//! - **Multi-day search.**
//! [`gtfs::GtfsTimetable::new_with_overnight_days`] takes an
//! [`gtfs::OvernightDays`] count and loads that many additional
//! service days after the base date, shifting day-`d` trips by
//! `d × 86 400` seconds. Lets a late-night query find an early
//! trip the next morning.
//! - **Sparse `transfers.txt`.** [`gtfs::GtfsTimetable::with_walking_footpaths`]
//! builds bidirectional walking edges from stop coordinates using an R-tree.
//!
//! # Custom backends
//!
//! Implement the [`Timetable`] trait directly if your data is not in GTFS
//! form. Identifiers are dense `u32` newtypes ([`StopIdx`], [`RouteIdx`],
//! [`TripIdx`]); your adapter interns external IDs to dense indices at
//! construction. The trait carries one mandatory soundness contract —
//! no-overtaking within a route – documented on [`Timetable`]. Footpaths
//! returned by [`Timetable::get_footpaths_from`] describe direct walks only;
//! the algorithm chains them within a round, so transitive closure of the
//! footpath relation is not required (see the [`Timetable`] trait's
//! Footpaths section for what closure means and when it matters).
//! Adapters whose relation *is* closed can opt into a faster single-pass
//! relaxation via [`Timetable::footpaths_are_transitively_closed`].
//!
//! [`manual::SimpleTimetable`] is a hand-rolled in-memory adapter useful for
//! tests and small fixtures.
//!
//! For an issue-by-issue walk through the algorithmic correctness of the
//! implementation against the paper, see
//! [`docs/soundness.md`](https://github.com/urschrei/vulture/blob/main/docs/soundness.md)
//! in the repository.
//!
//! # Errors
//!
//! The library uses three patterns, picked per-call to match what the
//! caller can usefully do:
//!
//! - **`Result` for construction.** Building a [`gtfs::GtfsTimetable`]
//! from a parsed feed validates many invariants and returns
//! [`Result<_, gtfs::GtfsError>`](gtfs::GtfsError). Custom adapters
//! should follow the same pattern with their own typed error enum.
//! - **`Option` for lookups.** Resolving an external ID
//! ([`gtfs::GtfsTimetable::stop_idx`], [`gtfs::GtfsTimetable::route_idx`])
//! returns `Option` – `None` simply means "not in this timetable".
//! Same for any "find me an `X` matching `Y`" accessor; there's no
//! useful structured error.
//! - **`Result` for plan reconstruction.** [`Journey::with_timing`]
//! returns [`Result<_, TimingError>`] – failure modes (no boarding
//! stop reachable, no catchable trip, alighting stop not on route)
//! are surfaced as variants because they carry useful debug context.
//! - **Panics for programmer-violated invariants.** Passing a
//! [`RaptorCache`] sized for one timetable to a query against a
//! differently-sized timetable panics – the mistake is unrecoverable
//! and the assertion message is the diagnostic. Custom [`Timetable`]
//! adapters that violate the no-overtaking contract documented on
//! the trait will likely produce wrong answers rather than panic.
//!
//! There is no all-encompassing `vulture::Error` enum – each failure
//! lives at a clear boundary, so unifying them would lose information
//! rather than add it.
//!
//! # Cargo features
//!
//! - `parallel` (default-on) – pulls in `rayon`, enables [`Query::run_par`] /
//! [`Query::run_with_pool`]. Opt out with `default-features = false` for
//! wasm or minimal builds; [`RaptorCachePool`] itself stays available.
//! - `gtfs-bench` – enables the `gtfs` criterion benchmark.
//! - `internal` – enables the `raptor` criterion benchmark over [`manual`].
/// In-memory `Timetable` adapter you build by hand with `.route(...)` /
/// `.footpath(...)` calls. Useful when your data isn't from a parsed
/// GTFS feed – for tests, custom data sources, and toy examples.
// Central items: the trait, the builder, the result.
pub use Timetable;
pub use Query;
pub use RangeJourney;
pub use Journey;
pub use TimedLeg;
pub use TimingError;
// Cache reuse: scratch buffers for repeated queries.
pub use PooledCache;
pub use RaptorCache;
pub use RaptorCachePool;
// Label trait + the default single-criterion impl.
pub use ArrivalTime;
pub use Label;
pub use TripContext;
// Query builder typestate markers.
pub use NeedsDeparture;
pub use RangeDeparture;
pub use SingleDeparture;
// Endpoints – query input.
pub use Endpoints;
pub use IntoEndpoints;
// Newtypes for indices and time quantities.
pub use RouteIdx;
pub use StopIdx;
pub use TripIdx;
pub use Duration;
pub use ParseSecondOfDayError;
pub use SecondOfDay;
pub use Transfers;
pub use TryFromSignedDurationError;
/// Internal round-counter type, used only for indexing label arrays.
/// User-facing transfer caps are passed as [`Transfers`].
pub type K = usize;