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/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
- 5 MB of it and takes about 200 ms end to end. The browser build is 18 kB
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
- ## Routing profile
98
+ ## Vehicles
48
99
 
49
100
  Every tag decision lives behind a `RoutingProfile` rather than in the compiler.
50
- The bundled `car` profile resolves the OSM access hierarchy
51
- (`access` < `vehicle` < `motor_vehicle` < `motorcar`, most specific wins),
52
- direction (`oneway=yes|-1|reverse|no|reversible`, `junction=roundabout|circular`,
53
- and the implicit one-way default on `motorway`/`motorway_link`), and speed from
54
- `maxspeed`, `maxspeed:forward` and `maxspeed:backward`.
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
- about 18 kB gzipped, and it needs only `fetch` and `DecompressionStream`:
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 `car` profile reads, and what it deliberately does not.
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 key wins |
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`; km/h, `mph`, `km/h`, `kmh`, `knots`, `walk` |
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
- | Restriction scope | `via` node and `via` way; `restriction:motorcar`, `restriction:motor_vehicle` and `restriction:vehicle` override the generic key; `except` listing a car mode drops the relation |
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
- | Profiles other than `car` | Only `car` ships. `getRoutingProfile` throws for anything else. |
416
- | Conditional restrictions | `restriction:conditional`, `access:conditional` and time-of-day rules are ignored, so a time-limited restriction is treated as absent. |
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
- | `barrier`, `motor_vehicle:conditional`, toll avoidance | Not read. |
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` in `<output>.json` tells you how much of a region's
426
- restriction data was understood.
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