roadgraph-router-v2 0.2.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 +217 -2
- package/README.md +408 -24
- package/dist/{chunk-PXL65P3L.js → chunk-QSIKDM4A.js} +942 -316
- package/dist/index.d.ts +480 -66
- package/dist/index.js +201 -14
- package/dist/worker.js +2 -2
- package/package.json +1 -1
- package/scripts/roadgraph-compile.mjs +30 -7
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ 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
|
-
4.3 MB of it and takes about 270 ms end to end. The browser build is
|
|
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
11
|
## What a route here does and does not mean
|
|
@@ -15,17 +15,23 @@ say are forbidden** to the chosen vehicle. That is weaker than a statement
|
|
|
15
15
|
that the route is legal, or that the vehicle will physically fit, and the gap
|
|
16
16
|
matters most for a lorry.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
direction and signed limits; turn restriction relations;
|
|
20
|
-
|
|
21
|
-
one of them is
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
27
32
|
- **Seasonal and weather rules.** Winter closures, snow-chain requirements,
|
|
28
|
-
flood gates.
|
|
33
|
+
flood gates. A conditional value resting on weather, such as
|
|
34
|
+
`80 @ (wet)`, is counted but never acted on.
|
|
29
35
|
- **Temporary closures.** Roadworks, events, landslides. Nothing in a compiled
|
|
30
36
|
bundle changes after the compile, so a bundle is as current as the extract
|
|
31
37
|
it was built from and no more.
|
|
@@ -39,14 +45,14 @@ one of them is treated as open:
|
|
|
39
45
|
limit is treated as passable by any vehicle.
|
|
40
46
|
|
|
41
47
|
For a lorry this means a route may pass under a bridge too low for it, over a
|
|
42
|
-
structure too weak for it, or along a street
|
|
43
|
-
|
|
44
|
-
vehicle.** Anything safety-critical needs a surveyed freight dataset
|
|
45
|
-
routing engine that reads conditional restrictions.
|
|
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.
|
|
46
51
|
|
|
47
52
|
The compiler reports what it understood. `turnRestrictionsRead` against
|
|
48
|
-
`turnRestrictionsSkipped`,
|
|
49
|
-
|
|
53
|
+
`turnRestrictionsSkipped`, `barriersRead` and
|
|
54
|
+
`edgesWithConditionalRestrictions`, in `<output>.json`, tell you how much of a
|
|
55
|
+
region's restriction data was used.
|
|
50
56
|
|
|
51
57
|
## Install
|
|
52
58
|
|
|
@@ -230,6 +236,242 @@ from the bounding box. A truck does not change shape when it crosses a border.
|
|
|
230
236
|
> [What a route here does and does not mean](#what-a-route-here-does-and-does-not-mean)
|
|
231
237
|
> before relying on one of these routes for a heavy vehicle.
|
|
232
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
|
+
});
|
|
464
|
+
```
|
|
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
|
+
|
|
233
475
|
## Turn restrictions
|
|
234
476
|
|
|
235
477
|
The compiler reads OSM `type=restriction` relations and the routing graph
|
|
@@ -279,6 +521,39 @@ It bites hardest on walking and cycling, where small fragments are common: in
|
|
|
279
521
|
Monaco the pedestrian network is 97.6% one piece, and the remaining 2.4% is
|
|
280
522
|
enough to lose a route between two points a kilometre apart.
|
|
281
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
|
+
|
|
282
557
|
## Snapping to the road, not to the junction
|
|
283
558
|
|
|
284
559
|
Topology simplification keeps only junctions and way endpoints as routing
|
|
@@ -380,7 +655,8 @@ EFR2 chunks inside EFRB.
|
|
|
380
655
|
## Using it in a browser
|
|
381
656
|
|
|
382
657
|
The routing engine has no Node built-ins in it. Bundled for the browser it is
|
|
383
|
-
|
|
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`:
|
|
384
660
|
|
|
385
661
|
```ts
|
|
386
662
|
import { RoadRouter } from "roadgraph-router";
|
|
@@ -512,6 +788,105 @@ Downloads are roughly 35% smaller for short routes and 47% smaller for long
|
|
|
512
788
|
ones, and graph load is *faster* than before despite the varint decoding,
|
|
513
789
|
because there is less to decompress.
|
|
514
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
|
+
|
|
515
890
|
### Nothing routes differently
|
|
516
891
|
|
|
517
892
|
`tools/compare-bundles.mjs` routes the same random pairs through two bundles
|
|
@@ -599,31 +974,40 @@ What the bundled profiles read, and what they deliberately do not.
|
|
|
599
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 |
|
|
600
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 |
|
|
601
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` |
|
|
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 |
|
|
602
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 |
|
|
603
980
|
|
|
604
981
|
**Not supported**
|
|
605
982
|
|
|
606
983
|
| Area | Behaviour |
|
|
607
984
|
| --- | --- |
|
|
608
|
-
| A one-way trap | Two points joined on the map but not reachable in the direction asked for
|
|
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. |
|
|
609
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. |
|
|
610
|
-
| Alternative routes | Not in 0.2.0. `routeAlternatives` is planned for 0.3.0. |
|
|
611
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. |
|
|
612
988
|
| Axle load, trailer and hazmat rules | `maxaxleload`, `hazmat` and trailer-specific access are not read. |
|
|
613
|
-
| Conditional restrictions
|
|
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. |
|
|
614
997
|
| Temporary closures | Roadworks, events and incidents are not modelled. A bundle is as current as the extract it was compiled from. |
|
|
615
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. |
|
|
616
999
|
| `via` ways longer than four edges | Skipped rather than approximated, and counted as `turnRestrictionsSkipped` in the compile report. |
|
|
617
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. |
|
|
618
1001
|
| Turn costs and traffic signals | Not modelled; a turn costs nothing beyond the roads it joins. |
|
|
619
1002
|
| Live traffic, elevation, ferries as a distinct mode | Not modelled. Ferry ways are excluded with everything that is not a road. |
|
|
620
|
-
|
|
|
1003
|
+
| Toll avoidance | Not read. |
|
|
621
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. |
|
|
622
1005
|
| EFRT v1 | Carries topology only: encoding to it drops geometry and turn restrictions. Use EFR2 for anything that needs them. |
|
|
623
1006
|
|
|
624
1007
|
The compiler reports what it skipped. `turnRestrictionsRead` against
|
|
625
|
-
`turnRestrictionsSkipped
|
|
626
|
-
restriction data was
|
|
1008
|
+
`turnRestrictionsSkipped`, and `edgesWithConditionalRestrictions`, in
|
|
1009
|
+
`<output>.json` tell you how much of a region's restriction data was
|
|
1010
|
+
understood.
|
|
627
1011
|
|
|
628
1012
|
## Upgrading from 0.1.0
|
|
629
1013
|
|