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 +348 -1
- package/README.md +729 -18
- package/dist/chunk-QSIKDM4A.js +2690 -0
- package/dist/index.d.ts +966 -89
- package/dist/index.js +624 -82
- package/dist/worker.js +2 -2
- package/package.json +2 -2
- package/scripts/roadgraph-compile.mjs +307 -76
- package/dist/chunk-4FIOQAFG.js +0 -1333
package/README.md
CHANGED
|
@@ -5,9 +5,55 @@ routing bundle, then route over it with A* — in Node.js or in the browser, wit
|
|
|
5
5
|
no routing service involved.
|
|
6
6
|
|
|
7
7
|
A 425 MB Portugal extract compiles to a 38 MB bundle. A city route downloads
|
|
8
|
-
|
|
8
|
+
4.3 MB of it and takes about 270 ms end to end. The browser build is 24.5 kB
|
|
9
9
|
gzipped and pulls in no Node built-ins.
|
|
10
10
|
|
|
11
|
+
## What a route here does and does not mean
|
|
12
|
+
|
|
13
|
+
A route is a path over roads that the compiled OpenStreetMap data **does not
|
|
14
|
+
say are forbidden** to the chosen vehicle. That is weaker than a statement
|
|
15
|
+
that the route is legal, or that the vehicle will physically fit, and the gap
|
|
16
|
+
matters most for a lorry.
|
|
17
|
+
|
|
18
|
+
Four kinds of rule are read and obeyed: tags on the way, such as access,
|
|
19
|
+
direction and signed limits; turn restriction relations; barriers on nodes;
|
|
20
|
+
and, when you supply a departure time, conditional restrictions on the way.
|
|
21
|
+
Everything below is **not read**, and a road governed only by one of them is
|
|
22
|
+
treated as open:
|
|
23
|
+
|
|
24
|
+
- **Conditional restrictions, unless you pass `departAt`.** Without a
|
|
25
|
+
departure time a street closed to lorries overnight or a square
|
|
26
|
+
pedestrianised on Sundays appears permanently open. See
|
|
27
|
+
[Routing at a time of day](#routing-at-a-time-of-day) for what is read when
|
|
28
|
+
you do pass one, and what is still not.
|
|
29
|
+
- **Conditional turn restrictions.** `restriction:conditional` on a turn
|
|
30
|
+
restriction relation is not read, so a no-left-turn that applies only in the
|
|
31
|
+
morning peak is enforced at every hour of the day.
|
|
32
|
+
- **Seasonal and weather rules.** Winter closures, snow-chain requirements,
|
|
33
|
+
flood gates. A conditional value resting on weather, such as
|
|
34
|
+
`80 @ (wet)`, is counted but never acted on.
|
|
35
|
+
- **Temporary closures.** Roadworks, events, landslides. Nothing in a compiled
|
|
36
|
+
bundle changes after the compile, so a bundle is as current as the extract
|
|
37
|
+
it was built from and no more.
|
|
38
|
+
- **Legal limits not expressed on the way.** National or urban default speed
|
|
39
|
+
limits, lorry bans covering a zone rather than a road, designated freight
|
|
40
|
+
routes, low-emission zones, tolls, permits.
|
|
41
|
+
- **Axle load, hazardous goods and trailer rules.** `maxaxleload`, `hazmat`
|
|
42
|
+
and trailer-specific access are not read.
|
|
43
|
+
- **An untagged limit.** `maxheight` is tagged on a minority of bridges
|
|
44
|
+
worldwide. Absence of a tag is not a measurement, and a road stating no
|
|
45
|
+
limit is treated as passable by any vehicle.
|
|
46
|
+
|
|
47
|
+
For a lorry this means a route may pass under a bridge too low for it, over a
|
|
48
|
+
structure too weak for it, or along a street banned to it by a rule nobody
|
|
49
|
+
tagged on the way. **Do not use this engine as the sole basis for dispatching
|
|
50
|
+
a heavy vehicle.** Anything safety-critical needs a surveyed freight dataset.
|
|
51
|
+
|
|
52
|
+
The compiler reports what it understood. `turnRestrictionsRead` against
|
|
53
|
+
`turnRestrictionsSkipped`, `barriersRead` and
|
|
54
|
+
`edgesWithConditionalRestrictions`, in `<output>.json`, tell you how much of a
|
|
55
|
+
region's restriction data was used.
|
|
56
|
+
|
|
11
57
|
## Install
|
|
12
58
|
|
|
13
59
|
```sh
|
|
@@ -19,6 +65,11 @@ with `fetch` and `DecompressionStream` (Chrome 103+, Firefox 113+, Safari 16.4+)
|
|
|
19
65
|
The `roadgraph-compile` CLI needs **Node.js 22.5 or newer**, because it indexes
|
|
20
66
|
coordinates through the built-in `node:sqlite` module.
|
|
21
67
|
|
|
68
|
+
The package's `engines` field states the lower of the two, because installing
|
|
69
|
+
this package usually means using the library. Running the CLI on an older Node
|
|
70
|
+
is refused at startup with that explanation rather than part-way through an
|
|
71
|
+
extract.
|
|
72
|
+
|
|
22
73
|
## Quick start
|
|
23
74
|
|
|
24
75
|
Compile an extract, then route over it:
|
|
@@ -44,14 +95,44 @@ result.geometry; // a GeoJSON LineString of 38 points following the roads
|
|
|
44
95
|
`RoadRouter.open()` also accepts an already-decoded `RoadGraph`, which is
|
|
45
96
|
useful for embedded or application-managed data.
|
|
46
97
|
|
|
47
|
-
##
|
|
98
|
+
## Vehicles
|
|
48
99
|
|
|
49
100
|
Every tag decision lives behind a `RoutingProfile` rather than in the compiler.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
101
|
+
Five ship. `car`, `motorcycle` and `truck` read the same tags in the same
|
|
102
|
+
order and differ in the mode name at the bottom of each hierarchy, so a car
|
|
103
|
+
resolves `motorcar` where a motorcycle resolves `motorcycle` and a lorry
|
|
104
|
+
resolves `hgv`. `bicycle` and `pedestrian` are a different shape, described
|
|
105
|
+
below.
|
|
106
|
+
|
|
107
|
+
| | `car` | `motorcycle` | `truck` |
|
|
108
|
+
| --- | --- | --- | --- |
|
|
109
|
+
| Access key | `motorcar` | `motorcycle` | `hgv` |
|
|
110
|
+
| Speed ceiling | 150 km/h | 150 km/h | **90 km/h**, applied to every road |
|
|
111
|
+
| Mode-specific tags | `oneway:motorcar`, `maxspeed:motorcar`, `restriction:motorcar` | `oneway:motorcycle`, `maxspeed:motorcycle`, `restriction:motorcycle` | `oneway:hgv`, `maxspeed:hgv`, `restriction:hgv` |
|
|
112
|
+
| Physical limits | — | — | `maxheight`, `maxwidth`, `maxlength`, `maxweight` |
|
|
113
|
+
|
|
114
|
+
| | `bicycle` | `pedestrian` |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| Access keys | `access`, `vehicle`, `bicycle` | `access`, `foot` |
|
|
117
|
+
| Speed | 7–18 km/h by way class | 2–5 km/h by way class |
|
|
118
|
+
| Ceiling | 30 km/h | 7 km/h |
|
|
119
|
+
| One-way signs | obeyed, lifted by `oneway:bicycle=no` | ignored, unless `oneway:foot` |
|
|
120
|
+
| Reads `maxspeed` | only to go slower | only to go slower |
|
|
121
|
+
|
|
122
|
+
Walking and cycling are not motor vehicles with different numbers; three rules
|
|
123
|
+
change shape. A posted limit never makes anyone pedal harder, so the way class
|
|
124
|
+
decides the pace and `maxspeed` can only lower it. A one-way sign restrains
|
|
125
|
+
traffic, which a pedestrian is not and a cyclist often has a signed contraflow
|
|
126
|
+
out of. And the network itself differs: a footway or a path is the road here,
|
|
127
|
+
while a motorway is not part of it at all.
|
|
128
|
+
|
|
129
|
+
Steps are walkable at 2 km/h and not cyclable at all. Shared paths tagged as a
|
|
130
|
+
cycleway are walkable, because where walking is genuinely forbidden `foot=no`
|
|
131
|
+
says so.
|
|
132
|
+
|
|
133
|
+
A lorry is capped below the posted limit nearly everywhere, so `truck` caps
|
|
134
|
+
every speed rather than only bounding the A* heuristic: a truck graph reusing
|
|
135
|
+
car durations would give wrong arrival times on roads it is entitled to use.
|
|
55
136
|
|
|
56
137
|
`maxspeed` accepts plain km/h, `mph`, `km/h`, `kmh` and `knots`. Any value that
|
|
57
138
|
is not a number with an optional unit — `none`, `signals`, `variable`, or a
|
|
@@ -61,12 +142,336 @@ clamped to the profile range the A* heuristic assumes, so a mistagged
|
|
|
61
142
|
`maxspeed=400` cannot make the heuristic inadmissible.
|
|
62
143
|
|
|
63
144
|
```ts
|
|
64
|
-
import { carRoutingProfile } from "roadgraph-router";
|
|
145
|
+
import { carRoutingProfile, truckRoutingProfile } from "roadgraph-router";
|
|
65
146
|
|
|
66
147
|
carRoutingProfile.evaluateWay({ highway: "primary", maxspeed: "60 mph" });
|
|
67
148
|
// { forward: true, backward: true, forwardSpeedKph: 96.56, backwardSpeedKph: 96.56 }
|
|
149
|
+
|
|
150
|
+
truckRoutingProfile.evaluateWay({ highway: "motorway", maxheight: "4.2" });
|
|
151
|
+
// { ..., forwardSpeedKph: 90, forwardLimits: { heightMeters: 4.2 }, ... }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Compiling for several vehicles
|
|
155
|
+
|
|
156
|
+
One bundle can serve several vehicles. Nearly nine tenths of a compiled tile —
|
|
157
|
+
geometry, node coordinates, topology and distances — is the same whichever
|
|
158
|
+
vehicle is routing, so it is stored once and each vehicle adds only its own
|
|
159
|
+
durations, access mask, restriction mask and limits.
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
roadgraph-compile portugal.osm.pbf --output portugal.efrb --profile car,truck,motorcycle
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The extract is read once however many vehicles are asked for, and a reader
|
|
166
|
+
downloads the shared half of a tile plus the one vehicle it wants. On Portugal
|
|
167
|
+
that is 40.2 MB for three vehicles where three separate bundles would be about
|
|
168
|
+
114 MB, and switching vehicle mid-session costs about 2 MB rather than another
|
|
169
|
+
38.
|
|
170
|
+
|
|
171
|
+
A single vehicle with no physical limits still compiles to the one-chunk layout
|
|
172
|
+
it always has, byte for byte.
|
|
173
|
+
|
|
174
|
+
**Walking and cycling need their own bundle.** They use footways, paths, steps
|
|
175
|
+
and cycleways that a road graph correctly leaves out, so they are not another
|
|
176
|
+
column on the same network but a larger network. Compiled together, Portugal is
|
|
177
|
+
96.1 MB against 42.8 MB for the three motor vehicles — putting them in the same
|
|
178
|
+
file would have made every driver download the footpaths.
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
roadgraph-compile portugal.osm.pbf --output portugal-active.efrb --profile bicycle,pedestrian --max-memory-mb 3000
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Choosing a vehicle at runtime
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const router = await RoadRouter.open("portugal.efrb");
|
|
188
|
+
router.profiles; // ["car", "truck", "motorcycle"]
|
|
189
|
+
router.profile; // "car"
|
|
190
|
+
|
|
191
|
+
const lorry = router.withProfile("truck");
|
|
192
|
+
await lorry.route(origin, destination);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`withProfile` reuses every shared chunk already downloaded, which is what makes
|
|
196
|
+
switching cheap.
|
|
197
|
+
|
|
198
|
+
### Lorry size and weight
|
|
199
|
+
|
|
200
|
+
A truck bundle carries the height, width, length and weight limits its roads
|
|
201
|
+
state, and the vehicle decides which roads are usable. Limits are quantised
|
|
202
|
+
downwards, so a stored limit never permits what the sign forbids, and roads
|
|
203
|
+
stating no limit stay open.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
await RoadRouter.open("portugal.efrb", undefined, { profile: "truck", vehicle: "us-semi" });
|
|
207
|
+
await RoadRouter.open("portugal.efrb", undefined, { profile: "truck", vehicle: { preset: "articulated", heightMeters: 4.3 } });
|
|
208
|
+
await RoadRouter.open("portugal.efrb", undefined, { profile: "truck", vehicle: null }); // no filtering
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
| Preset | Height | Width | Length | Weight |
|
|
212
|
+
| --- | --- | --- | --- | --- |
|
|
213
|
+
| `van` | 2.6 m | 2.1 m | 6.0 m | 3.5 t |
|
|
214
|
+
| `rigid` | 3.8 m | 2.55 m | 10.0 m | 18 t |
|
|
215
|
+
| `articulated` *(default)* | 4.0 m | 2.55 m | 16.5 m | 40 t |
|
|
216
|
+
| `road-train` | 4.0 m | 2.55 m | 18.75 m | 44 t |
|
|
217
|
+
| `us-semi` | 4.11 m | 2.59 m | 22.0 m | 36.3 t |
|
|
218
|
+
|
|
219
|
+
A caller who says nothing still gets a vehicle, because refusing to route until
|
|
220
|
+
four dimensions are filled in is worse than routing for a stated default. The
|
|
221
|
+
resolved dimensions are on `router.vehicle`, so the default is visible rather
|
|
222
|
+
than hidden. The order is: whatever the caller passed, then whatever the bundle
|
|
223
|
+
was compiled to suggest with `--default-vehicle`, then `articulated`.
|
|
224
|
+
|
|
225
|
+
There is no globally neutral lorry — EU general circulation is 2.55 m wide,
|
|
226
|
+
16.5 m long and 40 t at no more than 4 m tall, while a US semi is wider and
|
|
227
|
+
taller but lighter — so `--default-vehicle us-semi` lets whoever compiles a
|
|
228
|
+
North American extract say so once, knowingly, instead of the library guessing
|
|
229
|
+
from the bounding box. A truck does not change shape when it crosses a border.
|
|
230
|
+
|
|
231
|
+
> **A truck route here is a route over roads not known to forbid the vehicle,
|
|
232
|
+
> which is weaker than a guarantee of legality or of physical fit.**
|
|
233
|
+
> `maxheight` is tagged on a minority of bridges worldwide and an untagged
|
|
234
|
+
> road is treated as open; time-dependent lorry bans, zone restrictions, axle
|
|
235
|
+
> load and hazardous goods rules are not read at all. See
|
|
236
|
+
> [What a route here does and does not mean](#what-a-route-here-does-and-does-not-mean)
|
|
237
|
+
> before relying on one of these routes for a heavy vehicle.
|
|
238
|
+
|
|
239
|
+
## Routing at a time of day
|
|
240
|
+
|
|
241
|
+
Some roads are only closed at certain hours. A street may be shut to lorries
|
|
242
|
+
overnight, a square pedestrianised on Sundays, a bridge limited by weight in
|
|
243
|
+
one season. OpenStreetMap writes these as `*:conditional` tags, and this
|
|
244
|
+
library reads them when — and only when — you say when the journey starts.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const router = await RoadRouter.open("portugal.efrb");
|
|
248
|
+
|
|
249
|
+
const route = await router.route(origin, destination, "fastest", {
|
|
250
|
+
departAt: new Date("2026-10-15T08:00:00"),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const alternatives = await router.routeAlternatives(origin, destination, "fastest", {
|
|
254
|
+
count: 3,
|
|
255
|
+
maxStretch: 1.4,
|
|
256
|
+
maxSharing: 0.75,
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Both work the same way in Node and in a browser: the only difference is that
|
|
261
|
+
a browser needs `fetch` and `DecompressionStream`, which it has.
|
|
262
|
+
|
|
263
|
+
`departAt` is in the **local time of the roads being travelled**. This library
|
|
264
|
+
does not convert time zones and does not consult a clock: a `Date` is read for
|
|
265
|
+
its weekday, hour, minute and month as your runtime reports them, so a caller
|
|
266
|
+
routing in a region other than its own must do the conversion first. You can
|
|
267
|
+
pass the parts directly instead of a `Date`:
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
// Monday, 08:00, in March.
|
|
271
|
+
await router.route(from, to, "fastest", {
|
|
272
|
+
departAt: { weekday: 0, minutes: 8 * 60, month: 3 },
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
A restriction is written in the local time of the road it is on. Where the
|
|
277
|
+
roads and the code are in different places, name the zone:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
await router.route(from, to, "fastest", {
|
|
281
|
+
departAt: new Date("2026-10-15T08:00:00Z"),
|
|
282
|
+
timeZone: "Europe/Lisbon",
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Public holidays are a calendar this library does not carry, so a rule written
|
|
287
|
+
`no @ (PH)` applies only if you say the day is one:
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
await router.route(from, to, "fastest", { departAt, publicHoliday: true });
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### What is read
|
|
294
|
+
|
|
295
|
+
Every bundle carries them, whether it holds one vehicle or several: a
|
|
296
|
+
one-vehicle bundle keeps them in its single chunk, and a multi-vehicle bundle
|
|
297
|
+
in the per-vehicle half of each tile, since the same road can be shut to a
|
|
298
|
+
lorry and open to a car.
|
|
299
|
+
|
|
300
|
+
A `value @ condition` list is read from the way's access tags — `access`,
|
|
301
|
+
`vehicle`, `motor_vehicle` and the profile's own key, such as `hgv` — and from
|
|
302
|
+
`maxspeed`. Where several rules match the same moment, the **last** one wins,
|
|
303
|
+
which is what OpenStreetMap specifies and why the tagging convention is
|
|
304
|
+
general first and specific last.
|
|
305
|
+
|
|
306
|
+
Conditions resting on time of day, weekday, month and the vehicle's own
|
|
307
|
+
dimensions are answered. `no @ (Mo-Fr 07:00-19:00)`,
|
|
308
|
+
`no @ (22:00-06:00)`, `no @ (Nov-Feb)` and `no @ (weight>7.5)` all work, as do
|
|
309
|
+
several windows in one rule and conditions combined with `AND`.
|
|
310
|
+
|
|
311
|
+
An access rule evaluating to `no`, `private`, `destination` or a permit-style
|
|
312
|
+
value closes the road. `destination` counts as closed because this library has
|
|
313
|
+
no notion of a journey's purpose, and treating "only if you live here" as open
|
|
314
|
+
would route strangers down a residents' street.
|
|
315
|
+
|
|
316
|
+
A conditional `maxspeed` slows the edges it applies to, and the route reports
|
|
317
|
+
the slower duration rather than the one it was planned against. A conditional
|
|
318
|
+
limit can only **slow** a road, never speed it up: the search's bound is built
|
|
319
|
+
from the profile's ceiling, and an edge quicker than the graph says would
|
|
320
|
+
break the guarantee that the route found is the best one.
|
|
321
|
+
|
|
322
|
+
### What is not
|
|
323
|
+
|
|
324
|
+
- **Nothing happens without `departAt`.** Leaving it out ignores conditional
|
|
325
|
+
restrictions entirely, which is how this library behaved before it could
|
|
326
|
+
read them.
|
|
327
|
+
- **Only closures are applied.** A road the plain tags already forbid stays
|
|
328
|
+
forbidden even where a conditional value would open it, because the compiled
|
|
329
|
+
mask has already dropped it. This errs towards refusing a road that is in
|
|
330
|
+
fact usable, which is the direction this library errs in everywhere else.
|
|
331
|
+
- **One moment, not an arrival time per edge.** This is the limitation to
|
|
332
|
+
understand before relying on any of it. Every road on the route is judged by
|
|
333
|
+
the moment the journey *starts*, not by when it would be reached. Leave
|
|
334
|
+
Lisbon at 07:00 on a six-hour drive and the roads near Porto are judged at
|
|
335
|
+
07:00, though you would arrive at one in the afternoon. A restriction that
|
|
336
|
+
lifts at 10:00 therefore still closes a road you would reach at noon, and
|
|
337
|
+
one that begins at 10:00 is missed entirely.
|
|
338
|
+
|
|
339
|
+
Doing better means a time-dependent shortest path, where each edge is
|
|
340
|
+
evaluated at the arrival time the search has accumulated. That changes the
|
|
341
|
+
cost function, the A* bound and the corridor logic together, and it is a
|
|
342
|
+
piece of work in its own right rather than something to bolt on. It is not
|
|
343
|
+
in this release, and **this engine should not be described as doing
|
|
344
|
+
time-dependent routing**: it applies a departure time, which is a weaker
|
|
345
|
+
thing. The gap matters most on long journeys and least on urban ones, where
|
|
346
|
+
the whole route is driven inside the hour it began.
|
|
347
|
+
- **Conditions only you can answer wait for you to answer them.** Whether it
|
|
348
|
+
is raining, who is driving and what the journey is for are not things a
|
|
349
|
+
compiled graph knows. Tell it, and the rules resting on them apply:
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
await router.route(from, to, "fastest", {
|
|
353
|
+
departAt,
|
|
354
|
+
circumstances: { conditions: ["wet"], purposes: ["delivery"] },
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Unanswered, such a rule never holds, so a road shut `@ (wet)` stays open
|
|
359
|
+
until someone says it is wet. A stay duration is still not read at all. A rule resting on one — `80 @ (wet)` — is parsed, counted and then
|
|
360
|
+
ignored, leaving the road open. `unreadableConditions` reports how many of a
|
|
361
|
+
tag's rules fell into this.
|
|
362
|
+
- **Turn restriction relations are not conditional.** See the list in
|
|
363
|
+
[What a route here does and does not mean](#what-a-route-here-does-and-does-not-mean).
|
|
364
|
+
|
|
365
|
+
### Everything a route can be told
|
|
366
|
+
|
|
367
|
+
| Option | Meaning |
|
|
368
|
+
| --- | --- |
|
|
369
|
+
| `departAt` | When the journey starts, as a `Date` or as `{ weekday, minutes, month, dayOfMonth }`. Without it, conditional restrictions are ignored. |
|
|
370
|
+
| `timeZone` | The IANA zone the roads are in, such as `"Europe/Lisbon"`. Without it a `Date` is read as this machine reads it. |
|
|
371
|
+
| `publicHoliday` | Whether that day is one where the roads are. |
|
|
372
|
+
| `circumstances` | What the map cannot know: `{ conditions, userGroups, purposes }`. |
|
|
373
|
+
| `avoid` | Circles of road that are shut, as `{ lat, lng, radiusMeters }`. |
|
|
374
|
+
| `vehicle` | The lorry being routed, where the graph states physical limits. |
|
|
375
|
+
| `finishOnANearbyRoad` | `false` to be refused outright when the point cannot be reached. |
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
const route = await router.route(origin, destination, "fastest", {
|
|
379
|
+
departAt: new Date("2026-10-15T08:00:00Z"),
|
|
380
|
+
timeZone: "Europe/Lisbon",
|
|
381
|
+
circumstances: { conditions: ["wet"] },
|
|
382
|
+
avoid: [{ lat: 38.7223, lng: -9.1393, radiusMeters: 150 }],
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## More than one way there
|
|
387
|
+
|
|
388
|
+
`routeAlternatives` returns several routes, best first, for offering a choice
|
|
389
|
+
rather than a single answer.
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
const routes = await router.routeAlternatives(from, to, "fastest", { count: 3 });
|
|
393
|
+
// routes[0] is exactly what route() would have returned.
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The method is the straightforward one: find the best route, make the roads it
|
|
397
|
+
used dearer to the search than they really are, and search again. A road that
|
|
398
|
+
is the only sensible way through survives the penalty and is used again; one
|
|
399
|
+
that was merely convenient gives way to its neighbour. **The penalty never
|
|
400
|
+
reaches the answer** — every route is reported at the graph's own cost, so an
|
|
401
|
+
alternative's distance and duration are what it would really take.
|
|
402
|
+
|
|
403
|
+
Candidates are filtered rather than trusted. One that is much worse than the
|
|
404
|
+
best, or that is mostly the same road, is not an alternative anybody wants:
|
|
405
|
+
|
|
406
|
+
Each is checked rather than trusted: a `count` that is not a whole number
|
|
407
|
+
between 1 and 10, a `maxStretch` below 1, a `maxSharing` outside nought to
|
|
408
|
+
one, or a `penaltyFactor` below 1 raises a `RangeError` rather than searching
|
|
409
|
+
forever or returning something arbitrary.
|
|
410
|
+
|
|
411
|
+
| Option | Default | Meaning |
|
|
412
|
+
| --- | --- | --- |
|
|
413
|
+
| `count` | 3 | How many routes to return at most, the best one included. |
|
|
414
|
+
| `maxStretch` | 1.4 | How much worse than the best an alternative may be. |
|
|
415
|
+
| `maxSharing` | 0.75 | How much of the routes already offered it may reuse, by distance. |
|
|
416
|
+
| `penaltyFactor` | 2 | How much dearer a road already used is made to look. |
|
|
417
|
+
| `searchWiderCorridor` | `true` | Whether to fetch the wider corridor an alternative might lie in. `false` keeps the download to what one route needed. |
|
|
418
|
+
|
|
419
|
+
Routes come back in the order they would be driven: the best first, then the
|
|
420
|
+
rest by what they really cost. The penalties steer the search and never reach
|
|
421
|
+
the figures, so an alternative's distance and duration are its own.
|
|
422
|
+
|
|
423
|
+
Two routes count as the same road where they travel the same tarmac, measured
|
|
424
|
+
by distance travelled rather than by edges in common. A road driven in the
|
|
425
|
+
opposite direction is the same road. Two roads that merely join the same pair
|
|
426
|
+
of junctions are not: a slip road beside the road it leaves, or a lane round
|
|
427
|
+
the far side of a square, is a genuine second way there, and the compiled
|
|
428
|
+
graph holds a great many of them — 12,015 such pairs in the Porto to Lisbon
|
|
429
|
+
corridor alone. A junction two routes pass through costs no distance and so
|
|
430
|
+
does not make them similar. The roads a route only part-drives at its two
|
|
431
|
+
ends count for exactly what it drives of them, which
|
|
432
|
+
`RouteSnap.travelledMeters` reports.
|
|
433
|
+
|
|
434
|
+
**Fewer routes than you asked for is the normal answer.** A road network
|
|
435
|
+
usually has one obvious way through, and returning a second route that is
|
|
436
|
+
three-quarters the same road, or an hour longer, would be worse than
|
|
437
|
+
returning one. `routeAlternatives` always returns at least the best route, and
|
|
438
|
+
raises whatever `route` would raise when there is none.
|
|
439
|
+
|
|
440
|
+
Alternatives cost roughly one search each. Measured on the Portugal bundle,
|
|
441
|
+
Porto to Lisbon returned three routes — 312.0 km, 319.0 km and 350.5 km — in
|
|
442
|
+
3.4 s against 2.5 s for the single route, downloading 3.0 MB more than the
|
|
443
|
+
29.2 MB the first route needed. A short urban route in Lisbon returned two in
|
|
444
|
+
79 ms with nothing further to download.
|
|
445
|
+
|
|
446
|
+
Over a bundle this deliberately downloads a wider corridor than one route
|
|
447
|
+
needs: an alternative may be worse than the best by as much as `maxStretch`,
|
|
448
|
+
and the tiles holding it lie outside the corridor that proved the best route.
|
|
449
|
+
|
|
450
|
+
Everything a single route obeys, these obey: access, turn restrictions,
|
|
451
|
+
barriers, a lorry's dimensions, and conditional restrictions when you pass a
|
|
452
|
+
departure time.
|
|
453
|
+
|
|
454
|
+
## Roads you know are shut
|
|
455
|
+
|
|
456
|
+
A bundle is as current as the extract it was compiled from. Roadworks, an
|
|
457
|
+
incident, a street closed for a market: none of them is in it, and none of
|
|
458
|
+
them can be. What a caller reading a feed of such things can do is say so:
|
|
459
|
+
|
|
460
|
+
```js
|
|
461
|
+
const route = await router.route(from, to, "fastest", {
|
|
462
|
+
avoid: [{ lat: 38.7223, lng: -9.1393, radiusMeters: 150 }],
|
|
463
|
+
});
|
|
68
464
|
```
|
|
69
465
|
|
|
466
|
+
Anything a circle touches is closed to the search and to snapping, so a route
|
|
467
|
+
neither drives through a closure nor starts in the middle of one. It outranks
|
|
468
|
+
whatever the map says about the road being open, it applies to every
|
|
469
|
+
alternative, and it lasts only for that call — the graph is left as it was.
|
|
470
|
+
|
|
471
|
+
An area rather than a road, because a feed gives a place and a radius, not an
|
|
472
|
+
edge number. Only the roads near each circle are looked at, so closing a
|
|
473
|
+
junction costs the same whether the graph holds a town or a country.
|
|
474
|
+
|
|
70
475
|
## Turn restrictions
|
|
71
476
|
|
|
72
477
|
The compiler reads OSM `type=restriction` relations and the routing graph
|
|
@@ -99,6 +504,56 @@ A `via`-way run is enforced against the path actually taken, not against an
|
|
|
99
504
|
approximation. In a Portugal extract they are 541 runs against 33,796 pairs,
|
|
100
505
|
and they add 5 KB to a 71 MB bundle.
|
|
101
506
|
|
|
507
|
+
### When the nearest road is the wrong road
|
|
508
|
+
|
|
509
|
+
Snapping finds the nearest road, and near a disconnected stub — a path behind a
|
|
510
|
+
fence, a service road in a compound, a stretch clipped at the edge of a tile —
|
|
511
|
+
that is the stub. A route to a point beside one used to fail although a usable
|
|
512
|
+
road was a few metres further.
|
|
513
|
+
|
|
514
|
+
A failed route is now tried once more. The two ends are compared, and if they
|
|
515
|
+
sit in different pieces of the network, the end in the smaller piece is snapped
|
|
516
|
+
again, this time only to roads joined to the other end. Labelling the network
|
|
517
|
+
costs a pass over it, so it runs only after a failure and an ordinary route
|
|
518
|
+
never pays for it.
|
|
519
|
+
|
|
520
|
+
It bites hardest on walking and cycling, where small fragments are common: in
|
|
521
|
+
Monaco the pedestrian network is 97.6% one piece, and the remaining 2.4% is
|
|
522
|
+
enough to lose a route between two points a kilometre apart.
|
|
523
|
+
|
|
524
|
+
### When the nearest road cannot be reached
|
|
525
|
+
|
|
526
|
+
The road nearest a point is not always one the rest of the network can get to.
|
|
527
|
+
A pocket of one-way streets behind banned turns can be driven out of but never
|
|
528
|
+
into; an island of service roads around a terminal joins nothing at all. Asked
|
|
529
|
+
for a point on one of those, this library used to report no route.
|
|
530
|
+
|
|
531
|
+
It now finishes on a road a little further off, trying the nearest few in turn
|
|
532
|
+
and taking the first that works. `destinationSnap.distanceMeters` says how far
|
|
533
|
+
short of the point it finished, and `originSnap.distanceMeters` the same for
|
|
534
|
+
the start:
|
|
535
|
+
|
|
536
|
+
```js
|
|
537
|
+
const route = await router.route(from, to);
|
|
538
|
+
if (route.destinationSnap.distanceMeters > 100) {
|
|
539
|
+
// Finished on the nearest road it could reach, not at the point asked for.
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
This only happens after a route has already failed, so a reachable road
|
|
544
|
+
nearest the point is still always the one used. To be told plainly that a
|
|
545
|
+
point cannot be reached, turn it off:
|
|
546
|
+
|
|
547
|
+
```js
|
|
548
|
+
await router.route(from, to, "fastest", { finishOnANearbyRoad: false });
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Evora's walled centre is the case this was found on: `no_left_turn` and
|
|
552
|
+
`no_right_turn` relations on Rua de Serpa Pinto seal the only entry to a
|
|
553
|
+
street, so OpenStreetMap says no car may legally enter it. Routing Lisbon to a
|
|
554
|
+
point in that street now finishes 23 m away on the nearest road a car can
|
|
555
|
+
reach, rather than reporting no route across 133 km of motorway.
|
|
556
|
+
|
|
102
557
|
## Snapping to the road, not to the junction
|
|
103
558
|
|
|
104
559
|
Topology simplification keeps only junctions and way endpoints as routing
|
|
@@ -200,7 +655,8 @@ EFR2 chunks inside EFRB.
|
|
|
200
655
|
## Using it in a browser
|
|
201
656
|
|
|
202
657
|
The routing engine has no Node built-ins in it. Bundled for the browser it is
|
|
203
|
-
|
|
658
|
+
19.7 kB gzipped on its own, or 24.5 kB for everything this package exports,
|
|
659
|
+
and it needs only `fetch` and `DecompressionStream`:
|
|
204
660
|
|
|
205
661
|
```ts
|
|
206
662
|
import { RoadRouter } from "roadgraph-router";
|
|
@@ -221,6 +677,17 @@ import RoutingWorker from "roadgraph-router/worker?worker";
|
|
|
221
677
|
The `roadgraph-compile` CLI is Node-only and is never imported by the browser
|
|
222
678
|
entry point, so bundlers do not pull it in.
|
|
223
679
|
|
|
680
|
+
### Coordinates in a shared chunk
|
|
681
|
+
|
|
682
|
+
Nodes are numbered with locality, so each sits close to the one before. A
|
|
683
|
+
shared chunk writes the difference rather than the coordinate, which costs a
|
|
684
|
+
few bytes where the coordinate itself costs eight, and takes 6% off a
|
|
685
|
+
multi-vehicle bundle with nothing lost — the coordinates come back exactly.
|
|
686
|
+
|
|
687
|
+
The single-chunk layout a car bundle still uses is untouched, so a bundle
|
|
688
|
+
compiled for one vehicle stays byte-comparable and a 0.1.0 reader still opens
|
|
689
|
+
it.
|
|
690
|
+
|
|
224
691
|
## How a chunk is stored
|
|
225
692
|
|
|
226
693
|
EFR2 v6 keeps the same graph as earlier versions but writes it far more
|
|
@@ -321,6 +788,105 @@ Downloads are roughly 35% smaller for short routes and 47% smaller for long
|
|
|
321
788
|
ones, and graph load is *faster* than before despite the varint decoding,
|
|
322
789
|
because there is less to decompress.
|
|
323
790
|
|
|
791
|
+
### What conditional restrictions cost, and what they catch
|
|
792
|
+
|
|
793
|
+
Portugal compiled for car, truck and motorcycle, before and after the
|
|
794
|
+
restrictions were read:
|
|
795
|
+
|
|
796
|
+
Both compiled back to back on the same machine, so the two columns are
|
|
797
|
+
comparable:
|
|
798
|
+
|
|
799
|
+
| | 0.2.0 compiler | 0.3.0 compiler | |
|
|
800
|
+
| --- | --- | --- | --- |
|
|
801
|
+
| Final EFRB | 40.17 MB | **40.18 MB** | +14.8 kB, or +0.04% |
|
|
802
|
+
| Compile time | 8 min 01 s | 8 min 05 s | +1% |
|
|
803
|
+
| Peak RSS | 1.35 GB | 1.52 GB | +0.17 GB, holding the tag text |
|
|
804
|
+
| Routing edges | 2,760,171 | 2,760,171 | identical graph |
|
|
805
|
+
| Edges with a conditional restriction | 0 | **3,328** | |
|
|
806
|
+
|
|
807
|
+
Compiled for a car alone, where the whole graph is one chunk, the same extract
|
|
808
|
+
grows by 4,187 bytes — 38,037,856 to 38,042,043 — for 1,087 restrictions.
|
|
809
|
+
|
|
810
|
+
A region whose roads carry no conditional restriction compiles to the same
|
|
811
|
+
file as before, byte for byte, in either layout: Monaco for a car is 26,177
|
|
812
|
+
bytes from either compiler.
|
|
813
|
+
|
|
814
|
+
Asking for a departure time costs nothing measurable. Routing Porto to Lisbon
|
|
815
|
+
by car, three runs each: 2,294 ms cold and 208 ms repeated without one,
|
|
816
|
+
2,252 ms and 206 ms with one — a difference smaller than the spread between
|
|
817
|
+
runs.
|
|
818
|
+
|
|
819
|
+
The **Ponte 25 de Abril**, the bridge carrying the A2 into Lisbon, is tagged
|
|
820
|
+
`hgv:conditional = no @ (05:00-02:00)`: lorries may use it for three hours a
|
|
821
|
+
night. Routing Almada to Lisbon across it:
|
|
822
|
+
|
|
823
|
+
| Vehicle | Departure | Distance | Duration | Crossing |
|
|
824
|
+
| --- | --- | --- | --- | --- |
|
|
825
|
+
| Car | any hour | 12.2 km | 12 min | Ponte 25 de Abril |
|
|
826
|
+
| Truck | none given | 13.0 km | 14 min | Ponte 25 de Abril |
|
|
827
|
+
| Truck | 10:00 | **63.4 km** | **51 min** | Ponte Vasco da Gama |
|
|
828
|
+
| Truck | 03:00 | 13.0 km | 14 min | Ponte 25 de Abril |
|
|
829
|
+
|
|
830
|
+
The lorry without a departure time takes a bridge it is banned from for
|
|
831
|
+
twenty-one hours of the day. That is the gap this reads, and the 51-minute
|
|
832
|
+
detour is what obeying the sign actually costs.
|
|
833
|
+
|
|
834
|
+
Of Portugal's conditional values, the ones this library cannot answer are
|
|
835
|
+
left alone rather than guessed at. 555 trunk and primary roads carry
|
|
836
|
+
`maxspeed:conditional` values such as `90 @ sun`, which is not a condition
|
|
837
|
+
OpenStreetMap defines; they keep their posted limit.
|
|
838
|
+
|
|
839
|
+
### Against the earlier versions
|
|
840
|
+
|
|
841
|
+
0.1.0, 0.2.0 and this release reading the same file — the car bundle 0.1.0
|
|
842
|
+
published, 38.03 MB — so the comparison is of the engines rather than of
|
|
843
|
+
formats that changed alongside them. Cold is a fresh reader; repeated is the
|
|
844
|
+
same route asked for twice.
|
|
845
|
+
|
|
846
|
+
| Journey | 0.1.0 | 0.2.0 | 0.3.0 |
|
|
847
|
+
| --- | --- | --- | --- |
|
|
848
|
+
| Opening the bundle | 1 ms | 1 ms | 1 ms |
|
|
849
|
+
| Lisbon, across town | 245 / 14 ms | 289 / 8 ms | 290 / 9 ms |
|
|
850
|
+
| Bragança, rural | 269 / 140 ms | 251 / 4 ms | 281 / 3 ms |
|
|
851
|
+
| Porto to Lisbon | 2,715 / 2,104 ms | 1,991 / 281 ms | 1,976 / 284 ms |
|
|
852
|
+
| Porto to Faro | 3,653 / 3,258 ms | 2,253 / 427 ms | 2,244 / 416 ms |
|
|
853
|
+
| Braga to Évora | no route | no route | 6,176 / 3,166 ms |
|
|
854
|
+
|
|
855
|
+
Every version that can read a bundle finds the same roads: 312.02 km from
|
|
856
|
+
Porto to Lisbon and 553.20 km from Porto to Faro, on all three.
|
|
857
|
+
|
|
858
|
+
0.2.0 is where the long routes improved, and the gain is almost entirely in
|
|
859
|
+
asking twice: the corridor it has already stitched is kept, so a repeated
|
|
860
|
+
Porto–Faro route went from 3.3 s to 0.4 s. **0.3.0 routes at the same speed as
|
|
861
|
+
0.2.0** — the differences above are smaller than the spread between runs of
|
|
862
|
+
the same binary.
|
|
863
|
+
|
|
864
|
+
Braga to Évora is the exception, and it is not a slowdown: both earlier
|
|
865
|
+
versions spent their time failing. 0.2.0 widened its corridor to the whole
|
|
866
|
+
country over 7.9 s before reporting no route; 0.3.0 finishes on the nearest
|
|
867
|
+
road it can reach in 2.3 s. A point genuinely off the network still fails
|
|
868
|
+
about as fast as it always did, 810 ms against 866 ms.
|
|
869
|
+
|
|
870
|
+
### What each version can read
|
|
871
|
+
|
|
872
|
+
| | 0.1.0 bundle | 0.2.0 bundle | 0.3.0 bundle |
|
|
873
|
+
| --- | --- | --- | --- |
|
|
874
|
+
| 0.1.0 | yes | no | no |
|
|
875
|
+
| 0.2.0 | yes | yes | no |
|
|
876
|
+
| 0.3.0 | yes | yes | yes |
|
|
877
|
+
|
|
878
|
+
A 0.2.0 reader opens a 0.3.0 bundle and then fails on the first tile whose
|
|
879
|
+
roads carry a conditional restriction — the chunk is longer than it expects.
|
|
880
|
+
A bundle without any reads as before.
|
|
881
|
+
|
|
882
|
+
### What the package costs
|
|
883
|
+
|
|
884
|
+
| | 0.1.0 | 0.2.0 | 0.3.0 |
|
|
885
|
+
| --- | --- | --- | --- |
|
|
886
|
+
| Tarball | 46.4 kB | 75.9 kB | 91.0 kB |
|
|
887
|
+
| Unpacked | 182.8 kB | 289.3 kB | 342.1 kB |
|
|
888
|
+
| Bundled for a browser, gzipped | 13.6 kB | 21.1 kB | 24.8 kB |
|
|
889
|
+
|
|
324
890
|
### Nothing routes differently
|
|
325
891
|
|
|
326
892
|
`tools/compare-bundles.mjs` routes the same random pairs through two bundles
|
|
@@ -395,35 +961,180 @@ the cross-tile merge.
|
|
|
395
961
|
|
|
396
962
|
## Supported OSM features and limitations
|
|
397
963
|
|
|
398
|
-
What the
|
|
964
|
+
What the bundled profiles read, and what they deliberately do not.
|
|
399
965
|
|
|
400
966
|
**Supported**
|
|
401
967
|
|
|
402
968
|
| Area | Detail |
|
|
403
969
|
| --- | --- |
|
|
404
970
|
| Highway classes | `motorway` through `service` and `road`, including `_link` variants |
|
|
405
|
-
| Access | `access`, `vehicle`, `motor_vehicle`, `motorcar`, most specific
|
|
971
|
+
| Access | `access`, `vehicle`, `motor_vehicle`, then the vehicle's own key (`motorcar`, `motorcycle`, `hgv`); most specific wins |
|
|
406
972
|
| Direction | `oneway=yes/no/-1/reverse/true/false/1/0`, `junction=roundabout/circular`, implicit one-way on motorways |
|
|
407
|
-
| Speed | `maxspeed`, `maxspeed:forward`, `maxspeed:backward
|
|
973
|
+
| Speed | `maxspeed`, `maxspeed:forward`, `maxspeed:backward`, `maxspeed:<mode>`; km/h, `mph`, `km/h`, `kmh`, `knots`, `walk` |
|
|
974
|
+
| Lorry limits | `maxheight`, `maxwidth`, `maxlength`, `maxweight`, each with `:forward`, `:backward` and `:hgv` forms, plus `maxheight:physical` and `maxwidth:physical`; metric and feet-and-inches |
|
|
975
|
+
| Barriers | `barrier` on a node, with `locked` and the access ladder; a bollard stops a car, a stile stops everything but walking, a gate is open unless it says otherwise |
|
|
408
976
|
| Turn restrictions | `no_left_turn`, `no_right_turn`, `no_straight_on`, `no_u_turn`, `no_entry`, `no_exit`, `only_left_turn`, `only_right_turn`, `only_straight_on`, `only_u_turn` |
|
|
409
|
-
|
|
|
977
|
+
| Alternative routes | `routeAlternatives` returns several routes, best first, filtered by how much worse and how much the same they are |
|
|
978
|
+
| Conditional restrictions | `*:conditional` on the access ladder and on `maxspeed`, applied when a route supplies `departAt`; time of day, weekday, month, public holiday and vehicle dimensions; last matching rule wins |
|
|
979
|
+
| Restriction scope | `via` node and `via` way; `restriction:<mode>`, `restriction:motor_vehicle` and `restriction:vehicle` override the generic key; `except` listing one of this vehicle's modes drops the relation for that vehicle alone |
|
|
410
980
|
|
|
411
981
|
**Not supported**
|
|
412
982
|
|
|
413
983
|
| Area | Behaviour |
|
|
414
984
|
| --- | --- |
|
|
415
|
-
|
|
|
416
|
-
|
|
|
985
|
+
| A one-way trap | Two points joined on the map but not reachable in the direction asked for still cannot be joined. The route finishes on the nearest road it can reach instead, which `destinationSnap.distanceMeters` reports; `finishOnANearbyRoad: false` asks to be refused outright. |
|
|
986
|
+
| Surface, gradient and steps beyond a flat penalty | A cycling speed comes from the way class alone. `surface`, `smoothness` and elevation are not read, so a cobbled climb costs the same as smooth level tarmac. |
|
|
987
|
+
| Untagged limits | A road stating no `maxheight` or `maxweight` is treated as open to any lorry. Absence of a tag is not a measurement. |
|
|
988
|
+
| Axle load, trailer and hazmat rules | `maxaxleload`, `hazmat` and trailer-specific access are not read. |
|
|
989
|
+
| Conditional restrictions at the time a road is reached | Every road is judged by the moment the journey starts, not by when it would be reached. See [Routing at a time of day](#routing-at-a-time-of-day). This is not time-dependent routing. |
|
|
990
|
+
| Conditional restrictions without `departAt` | Read only when a route supplies a departure time; without one a time-limited restriction is treated as absent, as before. See [Routing at a time of day](#routing-at-a-time-of-day). |
|
|
991
|
+
| Conditional turn restrictions | `restriction:conditional` on a relation is not read, so a morning-peak no-left-turn is enforced at every hour. It is rare enough to be worth measuring rather than guessing at: 1 relation in 26,599 across Portugal. The compile report counts them as `conditionalTurnRestrictionsIgnored`. |
|
|
992
|
+
| Conditions resting on weather, user group or purpose | Read, but only a caller knows the answer. `circumstances: { conditions: ["wet"] }` makes `80 @ (wet)` apply; unanswered, the road stays open. A stay duration is still not read at all. |
|
|
993
|
+
| More than 65,536 distinct conditional values in one tile | The compile stops rather than dropping the surplus, which would turn a restricted road into an open one. Compile in smaller tiles with `--tile-size-km`. |
|
|
994
|
+
| Temporary closures in the bundle | Roadworks, events and incidents happen after a bundle is compiled, so none of them is in it. A caller reading a feed of them can pass `avoid` areas per route instead; see [Roads you know are shut](#roads-you-know-are-shut). |
|
|
995
|
+
| Reopening a road | Only closures are applied. `access=no` with `access:conditional=yes @ (...)` stays closed at every hour. |
|
|
996
|
+
| Time zones | `departAt` is taken as the local time of the roads travelled; no conversion is done and no clock is consulted. |
|
|
997
|
+
| Temporary closures | Roadworks, events and incidents are not modelled. A bundle is as current as the extract it was compiled from. |
|
|
998
|
+
| Zone-level rules | Lorry bans, low-emission zones, permit areas and tolls that apply to an area rather than to a way are not read. |
|
|
417
999
|
| `via` ways longer than four edges | Skipped rather than approximated, and counted as `turnRestrictionsSkipped` in the compile report. |
|
|
418
1000
|
| Restrictions spanning a tile boundary | Dropped for tiles that do not hold every edge of the run, rather than enforced in a shortened form. |
|
|
419
1001
|
| Turn costs and traffic signals | Not modelled; a turn costs nothing beyond the roads it joins. |
|
|
420
1002
|
| Live traffic, elevation, ferries as a distinct mode | Not modelled. Ferry ways are excluded with everything that is not a road. |
|
|
421
|
-
|
|
|
1003
|
+
| Toll avoidance | Not read. |
|
|
1004
|
+
| A barrier this table has not met | Treated as passable. Inventing a closure severs a road that is probably open, and the tag may simply be unfamiliar; an `access` tag on the node still decides. |
|
|
422
1005
|
| EFRT v1 | Carries topology only: encoding to it drops geometry and turn restrictions. Use EFR2 for anything that needs them. |
|
|
423
1006
|
|
|
424
1007
|
The compiler reports what it skipped. `turnRestrictionsRead` against
|
|
425
|
-
`turnRestrictionsSkipped
|
|
426
|
-
restriction data was
|
|
1008
|
+
`turnRestrictionsSkipped`, and `edgesWithConditionalRestrictions`, in
|
|
1009
|
+
`<output>.json` tell you how much of a region's restriction data was
|
|
1010
|
+
understood.
|
|
1011
|
+
|
|
1012
|
+
## Upgrading from 0.1.0
|
|
1013
|
+
|
|
1014
|
+
Nothing in the 0.1.0 API changed, and bundles compiled by 0.1.0 still open and
|
|
1015
|
+
route identically — the published Portugal bundle was re-read under 0.2.0 and
|
|
1016
|
+
returns the same 313.10 km Porto-to-Lisbon route.
|
|
1017
|
+
|
|
1018
|
+
What is worth knowing:
|
|
1019
|
+
|
|
1020
|
+
- **`engines.node` is now `>=18`**, matching the library rather than the
|
|
1021
|
+
compiler. The CLI still needs Node 22.5 for `node:sqlite` and says so at
|
|
1022
|
+
startup instead of part-way through an extract.
|
|
1023
|
+
- **Repeated routes are about eight times faster**, and first routes two to
|
|
1024
|
+
three times. The stitched corridor is cached between routes, one per vehicle
|
|
1025
|
+
a bundle holds; `mergedCacheSize: 0` restores the old behaviour of holding
|
|
1026
|
+
nothing, at the cost of re-stitching every time.
|
|
1027
|
+
- **Journey times rise by a fraction of a percent on a recompile.** A short
|
|
1028
|
+
edge whose duration rounded to zero used to be free to travel; it is now
|
|
1029
|
+
charged for. The route is the same, the estimate is honest. Reading an old
|
|
1030
|
+
bundle is unaffected, because the correction is in the data.
|
|
1031
|
+
- **Car graphs keep their layout.** A single-vehicle compile with no physical
|
|
1032
|
+
limits still writes the one-chunk layout, so an older reader opens a bundle
|
|
1033
|
+
compiled today. It does grow very slightly, because barriers now contribute
|
|
1034
|
+
banned turns.
|
|
1035
|
+
- **Barriers are read for the first time.** Gates, bollards and stiles were
|
|
1036
|
+
invisible to every profile before, so some routes that passed through one
|
|
1037
|
+
no longer do.
|
|
1038
|
+
- **New formats are additive.** EFR2 v7 and EFRB v2 appear only when a bundle
|
|
1039
|
+
holds several vehicles or a vehicle whose routes depend on its size; a 0.1.0
|
|
1040
|
+
reader cannot open those, which is why single-vehicle output stays v1.
|
|
1041
|
+
- **The car profile reads a few more tags** — `oneway:motor_vehicle`,
|
|
1042
|
+
`oneway:motorcar`, `maxspeed:motorcar` — so a route changes only where the
|
|
1043
|
+
data said it should all along.
|
|
1044
|
+
|
|
1045
|
+
## Benchmarks
|
|
1046
|
+
|
|
1047
|
+
Portugal, `portugal-latest.osm.pbf` at 425 MB, on one machine:
|
|
1048
|
+
|
|
1049
|
+
| | 0.1.0 car | 0.2.0 car | 0.2.0 car + truck + motorcycle |
|
|
1050
|
+
| --- | --- | --- | --- |
|
|
1051
|
+
| Bundle | 38.03 MB | 38.04 MB | **40.17 MB** |
|
|
1052
|
+
| Peak compile memory | 1.11 GB | 1.10 GB | 1.64 GB |
|
|
1053
|
+
| Compile time | 7m 46s | 7m 58s | 7m 48s |
|
|
1054
|
+
| Routing nodes | 1,244,119 | 1,244,119 | 1,244,263 |
|
|
1055
|
+
| Routing edges | 2,757,478 | 2,757,478 | 2,760,171 |
|
|
1056
|
+
| Barriers read | — | — | 33,236, giving 8,790 bans |
|
|
1057
|
+
|
|
1058
|
+
Three vehicles cost 5.6% more than one and compile in the same time, because
|
|
1059
|
+
the extract is read once however many are asked for. The same three as separate
|
|
1060
|
+
bundles would be about 114 MB.
|
|
1061
|
+
|
|
1062
|
+
Walking and cycling, compiled together into their own bundle:
|
|
1063
|
+
|
|
1064
|
+
| | bicycle + pedestrian |
|
|
1065
|
+
| --- | --- |
|
|
1066
|
+
| Bundle | 96.10 MB |
|
|
1067
|
+
| Peak compile memory | 2.71 GB |
|
|
1068
|
+
| Compile time | 16m 54s |
|
|
1069
|
+
| Routing nodes | 2,721,847 |
|
|
1070
|
+
| Routing edges | 6,870,455 |
|
|
1071
|
+
| Barriers read | 13,102, giving 9,409 bans |
|
|
1072
|
+
|
|
1073
|
+
Two and a half times the edges of the motor-vehicle bundle, because footways
|
|
1074
|
+
and paths exist where roads do not. It needs `--max-memory-mb 3000`.
|
|
1075
|
+
|
|
1076
|
+
### Routing
|
|
1077
|
+
|
|
1078
|
+
Urban, rural and long journeys over the two Portugal bundles, each measured
|
|
1079
|
+
cold and then repeated with nothing left to fetch:
|
|
1080
|
+
|
|
1081
|
+
| Journey | Vehicle | Result | First | Repeat | Downloaded |
|
|
1082
|
+
| --- | --- | --- | --- | --- | --- |
|
|
1083
|
+
| central Lisbon, 2 km | car | 2.4 km, 3 min | 270 ms | 9 ms | 4.3 MB |
|
|
1084
|
+
| | truck | 3.7 km, 5 min | 128 ms | 7 ms | 0.3 MB |
|
|
1085
|
+
| | motorcycle | 2.4 km, 3 min | 103 ms | 7 ms | 0.3 MB |
|
|
1086
|
+
| rural Bragança, 40 km | car | 41.0 km, 32 min | 238 ms | 5 ms | 3.0 MB |
|
|
1087
|
+
| | truck | 41.0 km, 35 min | 187 ms | 4 ms | 0.2 MB |
|
|
1088
|
+
| | motorcycle | 41.0 km, 32 min | 127 ms | 4 ms | 0.2 MB |
|
|
1089
|
+
| Porto to Lisbon, 313 km | car | 313.1 km, 165 min | 2245 ms | 257 ms | 22.2 MB |
|
|
1090
|
+
| | truck | 313.1 km, 218 min | 1767 ms | 140 ms | 1.5 MB |
|
|
1091
|
+
| | motorcycle | 313.1 km, 165 min | 1754 ms | 209 ms | 1.5 MB |
|
|
1092
|
+
| central Lisbon, 2 km | bicycle | 2.2 km, 8 min | 510 ms | 31 ms | 10.0 MB |
|
|
1093
|
+
| | pedestrian | 1.9 km, 23 min | 339 ms | 36 ms | 1.2 MB |
|
|
1094
|
+
| rural Bragança, 33 km | bicycle | 33.6 km, 146 min | 692 ms | 9 ms | 10.6 MB |
|
|
1095
|
+
| | pedestrian | 32.5 km, 411 min | 466 ms | 7 ms | 0.9 MB |
|
|
1096
|
+
|
|
1097
|
+
The lorry is 32% slower than the car over the same roads, which is the 90 km/h
|
|
1098
|
+
cap on motorways. The second and third vehicles cost a fraction of the first,
|
|
1099
|
+
because the road network was already downloaded and only their own columns
|
|
1100
|
+
remain. Opening a bundle costs two requests and about 4 kB, whatever its size,
|
|
1101
|
+
so a reader knows which vehicles are on offer before fetching anything.
|
|
1102
|
+
|
|
1103
|
+
A route repeated costs almost nothing: the stitched corridor is kept, one per
|
|
1104
|
+
vehicle the bundle holds, so moving between vehicles does not discard it.
|
|
1105
|
+
|
|
1106
|
+
Repeated routes against 0.1.0, which had none of this:
|
|
1107
|
+
|
|
1108
|
+
| | 0.1.0 | 0.2.0 |
|
|
1109
|
+
| --- | --- | --- |
|
|
1110
|
+
| Porto to Lisbon | 2107 ms | **257 ms** |
|
|
1111
|
+
| Lisbon to Faro | 2170 ms | **~140 ms** |
|
|
1112
|
+
|
|
1113
|
+
Three things account for it: the corridor is no longer re-stitched for every
|
|
1114
|
+
route, the stitch itself no longer builds a string key per node, and the search
|
|
1115
|
+
no longer remembers how it arrived at every junction in the country — only at
|
|
1116
|
+
the 1.3% that ban a turn.
|
|
1117
|
+
|
|
1118
|
+
Physical limits on Portugal: 7,652 of 2,760,171 edges state one, which is
|
|
1119
|
+
0.28%. That is why they are a side table rather than four columns — columns
|
|
1120
|
+
would cost about 11 MB to say nothing almost everywhere, against 61 kB.
|
|
1121
|
+
|
|
1122
|
+
### Tile size matters more for walking and cycling
|
|
1123
|
+
|
|
1124
|
+
A short urban bicycle route downloads 10 MB above, where the same journey by
|
|
1125
|
+
car downloads 4.3. Both fetch the tile they start in, and a tile of footpaths
|
|
1126
|
+
holds far more than a tile of roads.
|
|
1127
|
+
|
|
1128
|
+
Tile size is the lever: `--tile-size-km` trades a larger file, from more
|
|
1129
|
+
overlap between tiles, against a smaller first download. The default of 120 km
|
|
1130
|
+
suits a road network, and an active-travel bundle for a city is better served
|
|
1131
|
+
by something smaller.
|
|
1132
|
+
|
|
1133
|
+
At country scale this is currently out of reach. Compiling Portugal for
|
|
1134
|
+
walking and cycling at 40 km tiles exceeds the compiler's memory ceiling of
|
|
1135
|
+
3 GB and is refused rather than attempted — the guard does its job, but the
|
|
1136
|
+
measurement is one this release cannot make. A city-sized extract has no such
|
|
1137
|
+
trouble.
|
|
427
1138
|
|
|
428
1139
|
## Licence
|
|
429
1140
|
|