roadgraph-router-v2 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,343 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims
5
5
  to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.3.0] - 2026-10-15
8
+
9
+ ### Added
10
+
11
+ - **Conditional restrictions, when a route says when it starts.**
12
+ `router.route(from, to, "fastest", { departAt })` closes the roads shut at
13
+ that moment to both the search and the snapping, and slows the ones a
14
+ conditional speed limit slows. The reported duration is the slowed one, not
15
+ the one the route was planned against.
16
+
17
+ `*:conditional` is read from the access ladder — `access`, `vehicle`,
18
+ `motor_vehicle` and the profile's own key — and from `maxspeed`. Conditions
19
+ resting on time of day, weekday, month, public holiday and the vehicle's own
20
+ dimensions are answered, including windows that run past midnight, seasons
21
+ that span the new year and several windows in one rule. Where more than one
22
+ rule matches, the last wins, which is what OpenStreetMap specifies.
23
+
24
+ Measured on Portugal, where the Ponte 25 de Abril carries
25
+ `hgv:conditional = no @ (05:00-02:00)`: routing Almada to Lisbon, a lorry
26
+ with no departure time takes the bridge in 13.0 km, at 03:00 takes it in
27
+ 13.0 km, and at 10:00 detours 63.4 km over the Ponte Vasco da Gama. A car
28
+ crosses in 12.2 km at every hour. 3,328 of Portugal's 2,760,171 edges carry
29
+ a conditional restriction, costing 14.8 kB — the bundle grew from 40.17 MB
30
+ to 40.18 MB — and about 10 ms on a repeat long-distance route.
31
+
32
+ - **Alternative routes.** `routeAlternatives(from, to, preference, { count })`
33
+ returns several routes, best first. It finds the best one, makes the roads
34
+ it used dearer to the search than they really are, and searches again; the
35
+ penalty steers the search and never reaches the answer, so every route is
36
+ reported at the graph's own cost. Candidates far worse than the best, or
37
+ mostly along the same road, are rejected rather than offered, so fewer
38
+ routes than asked for is the normal answer on a network with one obvious
39
+ way through.
40
+
41
+ Porto to Lisbon returned three routes — 312.0 km, 319.0 km and 350.5 km —
42
+ in 3.4 s against 2.5 s for the single route, downloading 3.0 MB beyond the
43
+ 29.2 MB the first route needed. A short urban route returned two in 79 ms
44
+ with nothing further to download.
45
+
46
+ - **A departure may name the time zone the roads are in.** `timeZone:
47
+ "Europe/Lisbon"` reads the date there rather than wherever the code runs,
48
+ which is what a restriction written in local time means. Built on `Intl`;
49
+ nothing is bundled for it.
50
+
51
+ - **Weather, who is driving and what for can be answered.**
52
+ `circumstances: { conditions: ["wet"], userGroups: ["resident"],
53
+ purposes: ["delivery"] }` makes the rules resting on them apply.
54
+ Unanswered they never hold, so the behaviour of anyone not passing them is
55
+ unchanged. `unreadableConditions` now counts only what the parser genuinely
56
+ cannot read, which is what its name always claimed.
57
+
58
+ - **Roads the caller knows are shut can be avoided.** `avoid: [{ lat, lng,
59
+ radiusMeters }]` closes everything a circle touches, to the search and to
60
+ snapping alike, for that call only. Roadworks and incidents happen after a
61
+ bundle is compiled and cannot be in it; this is how a caller reading a feed
62
+ of them says so.
63
+
64
+ - **`searchWiderCorridor: false`** keeps an alternatives search to the tiles
65
+ one route needed, trading reach for about three megabytes on a long
66
+ journey.
67
+
68
+ - **The compile report counts `conditionalTurnRestrictionsIgnored`.** A turn
69
+ banned only at certain hours is still banned at every hour; the number now
70
+ says how much of a region that costs. Across Portugal it is 1 relation in
71
+ 26,599.
72
+
73
+ ### Fixed
74
+
75
+ - **A road is identified by its shape in order, not by a sum of its points.**
76
+ Adding coordinates up does not care which point came first, so a road
77
+ bending north then east and one bending east then north read as the same
78
+ road and one of them would be dropped as a duplicate alternative.
79
+
80
+ - **A closure standing on the date line shuts the roads under it.** The
81
+ projection it was tested against subtracted longitudes at face value, so a
82
+ road a few metres away measured half a world off.
83
+
84
+ - **A rule table too wide for its index stops the compile.** It used to drop
85
+ the surplus rows, which turned a restricted road into an unrestricted one
86
+ with nothing said. A tile holds up to 65,536 distinct conditional values;
87
+ past that the compile fails and names `--tile-size-km` as the way through.
88
+
89
+ - **Two roads joining the same pair of junctions are two roads.** Telling
90
+ alternatives apart by their endpoints alone treated a slip road and the road
91
+ beside it as one, hiding a genuine second way there. The Porto to Lisbon
92
+ corridor holds 12,015 such pairs, none the same length as its twin, so the
93
+ length now separates them while staying equal for the two directions of one
94
+ road.
95
+
96
+ - **Each end of a route counts what it actually drives.** The distance driven
97
+ on the part-roads a route begins and ends on was split evenly between the
98
+ two, which misreports overlap whenever one end joins its road near the far
99
+ junction. `RouteSnap.travelledMeters` now carries the real figure.
100
+
101
+ - **A month selector is read with the window it governs.**
102
+ `no @ (Oct 22:00-06:00)` stopped at midnight on the thirty-first instead of
103
+ running to six on the first of November. `TravelTime` may now carry
104
+ `dayOfMonth`; without one the answer is unchanged, which is right on thirty
105
+ days in thirty-one.
106
+
107
+ - **Alternative options are refused before anything is downloaded**, rather
108
+ than after a remote bundle has been asked for tiles.
109
+
110
+ - **A window named with a day is read as one window.** `no @ (Fr 22:00-06:00)`
111
+ was matched by asking "is it Friday?" and "is it between 22:00 and 06:00?"
112
+ separately, which answered both ends of it backwards: it applied at two on
113
+ Friday morning, before it began, and not at two on Saturday morning, while
114
+ it was still in force. A span running past midnight now belongs to the day
115
+ it starts on, which also fixes Sunday into Monday.
116
+
117
+ - **`24:30` is no longer a time.** It parsed as half past midnight, quietly
118
+ moving a window onto the following day. Only `24:00` is the end of a day.
119
+
120
+ - **A conditional speed limit keeps its units.** `30 mph @ (…)` was read with
121
+ `parseFloat` and became 30 km/h — two-thirds of the real limit, applied to
122
+ the road as fact. It now uses the same parser as a plain `maxspeed`.
123
+
124
+ - **A rule table too wide for its index drops its surplus.** Past 65,535
125
+ distinct values the extra rows were kept and left pointing at rule zero,
126
+ applying one road's restriction to another.
127
+
128
+ - **Alternatives search the corridor that was downloaded for them.** The
129
+ widening that fetches the tiles an alternative could lie in replaced the
130
+ stitched graph, but the search was handed the graph from before it, so the
131
+ tiles were downloaded and discarded. Asking for a single route no longer
132
+ widens the corridor at all.
133
+
134
+ - **Alternatives come back in the order they would be driven**, by their real
135
+ cost, rather than in the order the penalties happened to find them; and two
136
+ routes are told apart by the road they travel rather than by which direction
137
+ of it, so coming back along a road already offered is no longer a different
138
+ way there.
139
+
140
+ - **A bundle compiled for one vehicle now carries its conditional
141
+ restrictions.** They were stored in the per-vehicle chunk, which only exists
142
+ when a bundle holds several vehicles or one whose routes depend on its size.
143
+ A `--profile car` compile wrote the single-chunk layout instead, where
144
+ nothing held them, so every road read as permanently open however carefully
145
+ a route named its departure time. The restrictions now ride in that layout
146
+ too, in a header byte no chunk written before them ever used.
147
+
148
+ Found while comparing bundle sizes between releases: a car-only Portugal
149
+ compiled byte for byte the same under 0.2.0 and 0.3.0, which it should not
150
+ have, since 129 of its ways carry `access:conditional` and 85
151
+ `motor_vehicle:conditional`. Compiled for motorcycle alone, Monaco now
152
+ stores the Avenue Pasteur ban it used to drop, and a route across it travels
153
+ 183 m at midday and 3,081 m at 23:00 — the same answers the multi-vehicle
154
+ bundle gives.
155
+
156
+ - **Points on a road the network cannot reach no longer report no route.** The
157
+ road nearest a point is sometimes one nothing may legally enter: a pocket of
158
+ one-way streets behind banned turns, or an island of service roads. A route
159
+ to such a point failed outright, however much of the network lay between.
160
+
161
+ It now finishes on a road a little further off, trying the nearest few in
162
+ turn and taking the first that works, and `destinationSnap.distanceMeters`
163
+ reports how far short it stopped. `finishOnANearbyRoad: false` restores the
164
+ old behaviour of being told plainly that the point cannot be reached.
165
+
166
+ Found on Évora, where `no_left_turn` and `no_right_turn` relations on Rua de
167
+ Serpa Pinto seal the only entry to a street: Lisbon to a point there now
168
+ routes 133.2 km, finishing 23 m away, and Braga to Évora routes 460.2 km
169
+ where both previously failed. Lisbon airport, whose terminal roads are an
170
+ island 527 m from the nearest road a car can reach, routes too.
171
+
172
+ ### Added
173
+
174
+ - `edgesWithConditionalRestrictions` in the compile report, beside
175
+ `edgesWithPhysicalLimits`, so a region's conditional data can be counted
176
+ rather than assumed.
177
+
178
+ ### Known limitations
179
+
180
+ - **Nothing changes without `departAt`.** Omitting it ignores conditional
181
+ restrictions entirely, which is how the library behaved before.
182
+ - **Only closures are applied.** A road the plain tags already forbid stays
183
+ forbidden where a conditional value would open it, which errs towards
184
+ refusing a usable road rather than permitting a forbidden one.
185
+ - **One moment for the whole route**, rather than an arrival time per edge. A
186
+ long route is judged by when it set out.
187
+ - **`restriction:conditional` on a turn restriction relation is still not
188
+ read.** A morning-peak no-left-turn is enforced at every hour.
189
+ - **Conditions this library cannot answer are counted, not guessed.** Weather,
190
+ user groups, the purpose of access and a stay duration are parsed, reported
191
+ by `unreadableConditions`, and then left to one side with the road open.
192
+ - **`departAt` is the local time of the roads travelled.** No time-zone
193
+ conversion is done and no clock is consulted.
194
+
195
+ Measured against the earlier releases on one machine: routing is as fast as
196
+ 0.2.0 — Porto to Lisbon 2,252 ms cold and 206 ms repeated, against 2,252 and
197
+ 208 — and the compiler is 1% slower back to back, 8 min 05 s against
198
+ 8 min 01 s, for 0.17 GB more memory. The browser build grew from 21.1 kB
199
+ gzipped to 24.8 kB, and the published tarball from 75.9 kB to 91.0 kB.
200
+
201
+ Braga to Évora is the one journey that changed materially, and it changed
202
+ from failing to working: 0.2.0 spent 7.9 s widening its corridor before
203
+ reporting no route, where this release finishes in 2.3 s.
204
+
205
+ ### Compatibility
206
+
207
+ A bundle whose roads carry no conditional restriction is byte for byte the
208
+ file 0.2.0 wrote, in both layouts: the new sections live in header bytes that
209
+ every version so far left as zero. Monaco for a car is 26,177 bytes from
210
+ either compiler. Every bundle published so far still opens and routes
211
+ identically.
212
+
213
+ A bundle that does carry conditional restrictions cannot be opened by 0.2.0
214
+ or earlier, which report a truncated chunk.
215
+
216
+ ## [0.2.0] - 2026-10-15
217
+
218
+ Multi-vehicle routing. One bundle can now serve a car, a motorcycle and a
219
+ lorry, and a lorry's routes depend on its size and weight. Walking and cycling
220
+ ship too, in a bundle of their own. Alternative routes moved to 0.3.0.
221
+
222
+ ### Added
223
+
224
+ - **Coordinates in a shared chunk are written as differences**, which takes
225
+ about 6% off a multi-vehicle bundle and loses nothing: Portugal for walking
226
+ and cycling went from 102.3 MB to 96.1 MB, and the three motor vehicles from
227
+ 42.8 MB to 40.2 MB. The single-chunk layout a one-vehicle bundle uses is
228
+ untouched, so those stay readable by 0.1.0.
229
+ - **Barriers.** A gate, bollard or stile across a road is read from the node
230
+ and becomes a ban on passing through it, which still lets you turn back the
231
+ way you came. The same barrier stops different travellers differently: a
232
+ bollard is nothing on foot and the end of the road in a car, a stile is the
233
+ reverse. An unfamiliar barrier is left passable rather than guessed at.
234
+ - **`bicycle` and `pedestrian` profiles.** Not motor vehicles with different
235
+ numbers: a posted limit never makes anyone pedal harder, so the way class
236
+ decides the pace and `maxspeed` can only lower it; a one-way sign restrains
237
+ traffic, which a pedestrian is not and a cyclist often has a signed
238
+ contraflow out of; and footways, paths, steps and cycleways are the network
239
+ rather than roads left out of it. They need their own bundle — Portugal is
240
+ 102.3 MB for both against 42.8 MB for the three motor vehicles — because
241
+ folding them in would make every driver download the footpaths.
242
+ - **`motorcycle` and `truck` profiles.** The car profile became a factory
243
+ parameterised by the mode name, so all three read the same tags in the same
244
+ order and differ in the key at the bottom of each hierarchy: `motorcar`,
245
+ `motorcycle`, `hgv`. `truck` caps every speed at 90 km/h rather than only
246
+ bounding the A* heuristic, because a heavy vehicle is held below the posted
247
+ limit nearly everywhere.
248
+ - **Physical limits for lorries.** `maxheight`, `maxwidth`, `maxlength` and
249
+ `maxweight` are read per direction, preferring a limit signed for this
250
+ direction, then one signed for lorries, then the general one, and taking the
251
+ stricter of a signed and a `:physical` value. Feet and inches parse. A value
252
+ that states no number reads as unknown rather than as zero.
253
+ - **Vehicle presets** — `van`, `rigid`, `articulated`, `road-train`,
254
+ `us-semi` — resolved from the caller, then the bundle's own
255
+ `--default-vehicle`, then `articulated`. The resolved dimensions are on
256
+ `router.vehicle`, so a default is visible rather than hidden.
257
+ - **EFR2 v7 and EFRB v2.** A tile is written as a shared chunk holding the road
258
+ network and one small chunk per vehicle holding its durations, access mask,
259
+ restriction mask and limits. The directory lists them by `(tile, profile)`,
260
+ so a reader downloads the shared half plus the one vehicle it wants.
261
+ - **`--profile car,truck,motorcycle`** compiles one bundle for several
262
+ vehicles, reading the extract once.
263
+ - **`RoadRouter.profiles`, `RoadRouter.profile`, `RoadRouter.withProfile()`**
264
+ for reading a bundle as another vehicle, reusing every shared chunk already
265
+ downloaded.
266
+ - **Profile metadata in both formats**, in header bytes that were already
267
+ reserved, so nothing published grew.
268
+
269
+ ### Changed
270
+
271
+ - **Repeated routes are about four times faster.** The stitched corridor is
272
+ cached between routes, and the stitch itself no longer builds a string key
273
+ per node copy. Porto to Lisbon went from 2107 ms to 534 ms with nothing left
274
+ to download. `mergedCacheSize` controls it; `0` restores the old behaviour.
275
+ - **The A* heuristic and the tile budget take their speed ceiling from the
276
+ graph** rather than from the car profile. The heuristic stayed admissible
277
+ with the wrong ceiling but collapsed towards zero; the tile budget would have
278
+ over-downloaded badly for a slower vehicle.
279
+ - The car profile also reads `oneway:motor_vehicle`, `oneway:motorcar` and
280
+ `maxspeed:motorcar`, which only matter where those tags exist.
281
+
282
+ ### Fixed
283
+
284
+ - **`engines.node` was `>=22.5`**, the compiler's requirement, applied to the
285
+ whole package — so installing the library on Node 18 or 20 warned, and failed
286
+ under `engine-strict`. It is now `>=18`, and the CLI checks its own
287
+ requirement at startup rather than after streaming the whole extract.
288
+ - **Switching vehicle threw when the first one had dimensions.** Opening as a
289
+ lorry and asking for the car refused the switch, because the dimensions
290
+ travelled with the options and a car cannot use them. The check moved to
291
+ where a caller pairs a vehicle with a profile; past that, dimensions travel
292
+ with the reader and apply only where they mean something, so truck to car to
293
+ motorcycle and back ends with the dimensions it started with.
294
+ - **Switching vehicle discarded the stitched corridor.** A bundle kept two
295
+ corridors however many vehicles it held, so three vehicles evicted each
296
+ other and every switch re-stitched. It keeps one per vehicle now: a repeated
297
+ Porto to Lisbon fell from about 1500 ms to 257 ms by car, 140 ms by lorry.
298
+ - **A route could fail because an end snapped to a disconnected stub.** The
299
+ nearest road to a point is not always one that leads anywhere: a path behind
300
+ a fence, a service road in a compound, a stretch clipped at a tile edge. A
301
+ failed route is now retried against the piece of the network the other end
302
+ is in. It runs only after a failure, so an ordinary route never pays for it.
303
+ Walking and cycling networks feel this most, being full of small fragments.
304
+ - **Merging tiles dropped per-vehicle data.** Limits, access masks and
305
+ restriction masks are rebased onto the merged numbering, keeping the stricter
306
+ limit where two tiles disagree. A corridor built from more than one tile
307
+ would otherwise have routed a lorry under a bridge it does not fit through.
308
+ - A truck bundle written in the single-chunk layout, which has nowhere to keep
309
+ limits, is refused rather than routed with the dimensions silently ignored.
310
+
311
+ ### Documentation
312
+
313
+ - **The README now states plainly what a route does not guarantee.** A route
314
+ is a path over roads the data does not say are forbidden, which is weaker
315
+ than legal or physical suitability. Time-dependent access, seasonal and
316
+ weather rules, temporary closures, zone-level lorry bans and low-emission
317
+ zones, axle load and hazardous goods rules, and untagged limits are all
318
+ listed as unread, with the consequence for a heavy vehicle said directly:
319
+ a route may pass under a bridge too low for it or along a street it is
320
+ banned from at the hour you travel.
321
+
322
+ ### Known limitations
323
+
324
+ - **Conditional restrictions are not read in this release.**
325
+ `access:conditional`, `restriction:conditional` and `hgv:conditional` are
326
+ ignored, so a street closed to lorries overnight or a square pedestrianised
327
+ on Sundays appears permanently open. It is the limitation most likely to
328
+ make a route wrong rather than merely longer, particularly for a lorry, so
329
+ it was scheduled ahead of alternative routes and is addressed under
330
+ Unreleased above.
331
+ - An active-travel bundle at country scale cannot be compiled with small
332
+ tiles: Portugal for walking and cycling at 40 km tiles exceeds the
333
+ compiler's 3 GB ceiling and is refused. The default 120 km works, at the
334
+ cost of a larger first download for a short urban route.
335
+ - Alternative routes are not in this release; they are planned for 0.3.0,
336
+ after conditional restrictions.
337
+
338
+ ### Compatibility
339
+
340
+ Bundles compiled by 0.1.0 open and route identically. A single-vehicle compile
341
+ with no physical limits still writes the one-chunk layout byte for byte. The
342
+ new formats appear only when a bundle holds several vehicles or a vehicle whose
343
+ routes depend on its size, and a 0.1.0 reader cannot open those.
8
344
 
9
345
  ## [0.1.0] - 2026-10-12
10
346
 
@@ -39,6 +375,17 @@ package does rather than how it changed.
39
375
  - **Formats.** Reads EFRT v1 and EFR2 v2 through v6; writes EFR2 v6 by default
40
376
  and the v2-v5 layout on request.
41
377
 
378
+ ### Documentation
379
+
380
+ - **The README now states plainly what a route does not guarantee.** A route
381
+ is a path over roads the data does not say are forbidden, which is weaker
382
+ than legal or physical suitability. Time-dependent access, seasonal and
383
+ weather rules, temporary closures, zone-level lorry bans and low-emission
384
+ zones, axle load and hazardous goods rules, and untagged limits are all
385
+ listed as unread, with the consequence for a heavy vehicle said directly:
386
+ a route may pass under a bridge too low for it or along a street it is
387
+ banned from at the hour you travel.
388
+
42
389
  ### Known limitations
43
390
 
44
391
  See the "Supported OSM features and limitations" section of the README.