@osmix/core 0.1.10 → 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.
Files changed (97) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -72
  4. package/dist/contracts.d.ts +27 -0
  5. package/dist/contracts.d.ts.map +1 -0
  6. package/dist/contracts.js +1 -0
  7. package/dist/contracts.js.map +1 -0
  8. package/dist/entities.d.ts +22 -2
  9. package/dist/entities.d.ts.map +1 -1
  10. package/dist/entities.js +54 -10
  11. package/dist/entities.js.map +1 -1
  12. package/dist/ids.d.ts +20 -5
  13. package/dist/ids.d.ts.map +1 -1
  14. package/dist/ids.js +87 -29
  15. package/dist/ids.js.map +1 -1
  16. package/dist/index.d.ts +7 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +6 -4
  19. package/dist/index.js.map +1 -1
  20. package/dist/indirect-kd-index.d.ts +28 -0
  21. package/dist/indirect-kd-index.d.ts.map +1 -0
  22. package/dist/indirect-kd-index.js +163 -0
  23. package/dist/indirect-kd-index.js.map +1 -0
  24. package/dist/limits.d.ts +30 -0
  25. package/dist/limits.d.ts.map +1 -0
  26. package/dist/limits.js +51 -0
  27. package/dist/limits.js.map +1 -0
  28. package/dist/mocks.d.ts.map +1 -1
  29. package/dist/mocks.js.map +1 -1
  30. package/dist/nodes.d.ts +36 -15
  31. package/dist/nodes.d.ts.map +1 -1
  32. package/dist/nodes.js +185 -62
  33. package/dist/nodes.js.map +1 -1
  34. package/dist/osm.d.ts +60 -4
  35. package/dist/osm.d.ts.map +1 -1
  36. package/dist/osm.js +32 -6
  37. package/dist/osm.js.map +1 -1
  38. package/dist/relations.d.ts +11 -4
  39. package/dist/relations.d.ts.map +1 -1
  40. package/dist/relations.js +73 -29
  41. package/dist/relations.js.map +1 -1
  42. package/dist/renumber.d.ts +23 -0
  43. package/dist/renumber.d.ts.map +1 -0
  44. package/dist/renumber.js +64 -0
  45. package/dist/renumber.js.map +1 -0
  46. package/dist/stringtable.d.ts +2 -1
  47. package/dist/stringtable.d.ts.map +1 -1
  48. package/dist/stringtable.js +9 -14
  49. package/dist/stringtable.js.map +1 -1
  50. package/dist/tags.d.ts +45 -12
  51. package/dist/tags.d.ts.map +1 -1
  52. package/dist/tags.js +153 -27
  53. package/dist/tags.js.map +1 -1
  54. package/dist/typed-arrays.d.ts +63 -48
  55. package/dist/typed-arrays.d.ts.map +1 -1
  56. package/dist/typed-arrays.js +184 -107
  57. package/dist/typed-arrays.js.map +1 -1
  58. package/dist/ways.d.ts +52 -3
  59. package/dist/ways.d.ts.map +1 -1
  60. package/dist/ways.js +223 -46
  61. package/dist/ways.js.map +1 -1
  62. package/package.json +45 -31
  63. package/src/contracts.ts +29 -0
  64. package/src/entities.ts +242 -194
  65. package/src/ids.ts +278 -235
  66. package/src/index.ts +22 -13
  67. package/src/indirect-kd-index.ts +212 -0
  68. package/src/limits.ts +73 -0
  69. package/src/mocks.ts +114 -113
  70. package/src/nodes.ts +538 -397
  71. package/src/osm.ts +282 -207
  72. package/src/relations.ts +580 -558
  73. package/src/renumber.ts +80 -0
  74. package/src/stringtable.ts +189 -184
  75. package/src/tags.ts +406 -272
  76. package/src/typed-arrays.ts +307 -205
  77. package/src/types/geoflatbush.d.ts +28 -33
  78. package/src/ways.ts +577 -406
  79. package/test/entity-index-errors.test.ts +41 -0
  80. package/test/ids.bench.ts +33 -0
  81. package/test/ids.test.ts +122 -0
  82. package/test/limits.test.ts +83 -0
  83. package/test/memory.test.ts +29 -30
  84. package/test/no-shared-array-buffer.test.ts +41 -0
  85. package/test/nodes-spatial-index.test.ts +261 -0
  86. package/test/osm-bbox.test.ts +30 -0
  87. package/test/relations.bench.ts +32 -0
  88. package/test/relations.test.ts +124 -0
  89. package/test/renumber.test.ts +54 -0
  90. package/test/spatial-index-rebuild.test.ts +33 -0
  91. package/test/storage-compaction.test.ts +269 -0
  92. package/test/tags-search.test.ts +89 -56
  93. package/test/typed-arrays.test.ts +252 -132
  94. package/tsconfig.build.json +6 -6
  95. package/tsconfig.json +7 -7
  96. package/src/types/geokdbush.d.ts +0 -37
  97. package/src/types/kdbush.d.ts +0 -47
@@ -0,0 +1,212 @@
1
+ /**
2
+ * A compact static KD index over coordinate columns owned by another object.
3
+ *
4
+ * The selection and traversal algorithms are adapted from KDBush 4.1.0,
5
+ * Copyright (c) 2018 Vladimir Agafonkin, distributed under the ISC License.
6
+ * Unlike KDBush, this index stores only a Uint32 permutation. Coordinates are
7
+ * dereferenced from the existing Int32 microdegree columns.
8
+ */
9
+
10
+ import type { BufferType } from "./typed-arrays.ts";
11
+ import { BufferConstructor } from "./typed-arrays.ts";
12
+
13
+ const DEFAULT_NODE_SIZE = 64;
14
+
15
+ export class IndirectKdIndex {
16
+ readonly indexes: Uint32Array;
17
+ private readonly lons: Int32Array;
18
+ private readonly lats: Int32Array;
19
+ private readonly nodeSize: number;
20
+
21
+ constructor(
22
+ lons: Int32Array,
23
+ lats: Int32Array,
24
+ indexes: Uint32Array,
25
+ nodeSize = DEFAULT_NODE_SIZE,
26
+ ) {
27
+ this.lons = lons;
28
+ this.lats = lats;
29
+ this.indexes = indexes;
30
+ this.nodeSize = nodeSize;
31
+ }
32
+
33
+ static build(
34
+ lons: Int32Array,
35
+ lats: Int32Array,
36
+ count: number,
37
+ fill: (indexes: Uint32Array) => void,
38
+ ): IndirectKdIndex {
39
+ const buffer = new BufferConstructor(count * Uint32Array.BYTES_PER_ELEMENT);
40
+ const indexes = new Uint32Array(buffer);
41
+ fill(indexes);
42
+ sortKd(indexes, lons, lats, DEFAULT_NODE_SIZE, 0, indexes.length - 1, 0);
43
+ return new IndirectKdIndex(lons, lats, indexes);
44
+ }
45
+
46
+ static from(lons: Int32Array, lats: Int32Array, buffer: BufferType): IndirectKdIndex {
47
+ if (buffer.byteLength % Uint32Array.BYTES_PER_ELEMENT !== 0) {
48
+ throw new Error("Indirect KD index buffer length must be divisible by four");
49
+ }
50
+ return new IndirectKdIndex(lons, lats, new Uint32Array(buffer));
51
+ }
52
+
53
+ get buffer(): BufferType {
54
+ return this.indexes.buffer as BufferType;
55
+ }
56
+
57
+ /**
58
+ * Find entity indexes within an inclusive bounding box.
59
+ *
60
+ * Bounds are **integer microdegrees** (the units of the underlying Int32
61
+ * coordinate columns), not degrees. `Nodes.findIndexesWithinBbox` performs
62
+ * the degree → microdegree conversion.
63
+ */
64
+ range(minLon: number, minLat: number, maxLon: number, maxLat: number): number[] {
65
+ const stack = [0, this.indexes.length - 1, 0];
66
+ const result: number[] = [];
67
+
68
+ while (stack.length > 0) {
69
+ const axis = stack.pop();
70
+ const right = stack.pop();
71
+ const left = stack.pop();
72
+ if (axis === undefined || right === undefined || left === undefined) {
73
+ throw new Error("Indirect KD index traversal stack is corrupt");
74
+ }
75
+
76
+ if (right - left <= this.nodeSize) {
77
+ for (let i = left; i <= right; i++) {
78
+ const entityIndex = entityAt(this.indexes, i);
79
+ const lon = coordinate(this.lons, this.lats, entityIndex, 0);
80
+ const lat = coordinate(this.lons, this.lats, entityIndex, 1);
81
+ if (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat) {
82
+ result.push(entityIndex);
83
+ }
84
+ }
85
+ continue;
86
+ }
87
+
88
+ const middle = (left + right) >> 1;
89
+ const entityIndex = entityAt(this.indexes, middle);
90
+ const lon = coordinate(this.lons, this.lats, entityIndex, 0);
91
+ const lat = coordinate(this.lons, this.lats, entityIndex, 1);
92
+
93
+ if (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat) {
94
+ result.push(entityIndex);
95
+ }
96
+
97
+ const splitCoordinate = axis === 0 ? lon : lat;
98
+ const minimum = axis === 0 ? minLon : minLat;
99
+ const maximum = axis === 0 ? maxLon : maxLat;
100
+ if (minimum <= splitCoordinate) stack.push(left, middle - 1, 1 - axis);
101
+ if (maximum >= splitCoordinate) stack.push(middle + 1, right, 1 - axis);
102
+ }
103
+
104
+ return result;
105
+ }
106
+ }
107
+
108
+ function sortKd(
109
+ indexes: Uint32Array,
110
+ lons: Int32Array,
111
+ lats: Int32Array,
112
+ nodeSize: number,
113
+ left: number,
114
+ right: number,
115
+ axis: number,
116
+ ) {
117
+ if (right - left <= nodeSize) return;
118
+ const middle = (left + right) >> 1;
119
+ select(indexes, lons, lats, middle, left, right, axis);
120
+ sortKd(indexes, lons, lats, nodeSize, left, middle - 1, 1 - axis);
121
+ sortKd(indexes, lons, lats, nodeSize, middle + 1, right, 1 - axis);
122
+ }
123
+
124
+ /** Floyd-Rivest selection adapted to dereference coordinates indirectly. */
125
+ function select(
126
+ indexes: Uint32Array,
127
+ lons: Int32Array,
128
+ lats: Int32Array,
129
+ k: number,
130
+ initialLeft: number,
131
+ initialRight: number,
132
+ axis: number,
133
+ ) {
134
+ let left = initialLeft;
135
+ let right = initialRight;
136
+
137
+ while (right > left) {
138
+ if (right - left > 600) {
139
+ const n = right - left + 1;
140
+ const m = k - left + 1;
141
+ const z = Math.log(n);
142
+ const s = 0.5 * Math.exp((2 * z) / 3);
143
+ const sd = 0.5 * Math.sqrt((z * s * (n - s)) / n) * (m - n / 2 < 0 ? -1 : 1);
144
+ const newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd));
145
+ const newRight = Math.min(right, Math.floor(k + ((n - m) * s) / n + sd));
146
+ select(indexes, lons, lats, k, newLeft, newRight, axis);
147
+ }
148
+
149
+ const pivotIndex = entityAt(indexes, k);
150
+ const pivot = coordinate(lons, lats, pivotIndex, axis);
151
+ let i = left;
152
+ let j = right;
153
+
154
+ swap(indexes, left, k);
155
+ if (compareCoordinate(lons, lats, entityAt(indexes, right), pivotIndex, axis) > 0) {
156
+ swap(indexes, left, right);
157
+ }
158
+
159
+ while (i < j) {
160
+ swap(indexes, i, j);
161
+ i++;
162
+ j--;
163
+ while (i <= right && coordinate(lons, lats, entityAt(indexes, i), axis) < pivot) i++;
164
+ while (j >= left && coordinate(lons, lats, entityAt(indexes, j), axis) > pivot) j--;
165
+ }
166
+
167
+ if (coordinate(lons, lats, entityAt(indexes, left), axis) === pivot) {
168
+ swap(indexes, left, j);
169
+ } else {
170
+ j++;
171
+ swap(indexes, j, right);
172
+ }
173
+
174
+ if (j <= k) left = j + 1;
175
+ if (k <= j) right = j - 1;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Read the permutation entry at `position`, throwing on an out-of-bounds read.
181
+ * A silent skip here would leave the permutation subtly unsorted and produce
182
+ * wrong query results, so fail loudly instead.
183
+ */
184
+ function entityAt(indexes: Uint32Array, position: number): number {
185
+ const value = indexes[position];
186
+ if (value === undefined) {
187
+ throw new Error(`Indirect KD index read out of bounds at position ${position}`);
188
+ }
189
+ return value;
190
+ }
191
+
192
+ function coordinate(lons: Int32Array, lats: Int32Array, entityIndex: number, axis: number): number {
193
+ const value = axis === 0 ? lons[entityIndex] : lats[entityIndex];
194
+ if (value === undefined) throw new Error(`Missing coordinate for node index ${entityIndex}`);
195
+ return value;
196
+ }
197
+
198
+ function compareCoordinate(
199
+ lons: Int32Array,
200
+ lats: Int32Array,
201
+ a: number,
202
+ b: number,
203
+ axis: number,
204
+ ): number {
205
+ return coordinate(lons, lats, a, axis) - coordinate(lons, lats, b, axis);
206
+ }
207
+
208
+ function swap(indexes: Uint32Array, a: number, b: number) {
209
+ const valueA = entityAt(indexes, a);
210
+ indexes[a] = entityAt(indexes, b);
211
+ indexes[b] = valueA;
212
+ }
package/src/limits.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Hard capacity limits set by the typed-array layout of `Osm` storage.
3
+ *
4
+ * See `docs/limits.md` for the full list, including limits that come from the
5
+ * JavaScript engine rather than from this layout.
6
+ */
7
+
8
+ /** Largest number of node refs one way can hold (`Ways.refCount` is a `Uint16Array`). */
9
+ export const MAX_WAY_REFS = 0xffff;
10
+
11
+ /** Largest number of members one relation can hold (`Relations.memberCount` is a `Uint16Array`). */
12
+ export const MAX_RELATION_MEMBERS = 0xffff;
13
+
14
+ /** Largest UTF-8 byte length of one string (`StringTable.count` is a `Uint16Array`). */
15
+ export const MAX_STRING_BYTES = 0xffff;
16
+
17
+ /**
18
+ * Largest element offset a `Uint32Array` offset column can store. Bounds the total
19
+ * way refs, relation members and string-table bytes in one dataset.
20
+ */
21
+ export const MAX_UINT32_OFFSET = 0xffffffff;
22
+
23
+ /** The storage limit an {@link OsmCapacityError} reports. */
24
+ export type OsmCapacityLimit =
25
+ | "way-refs"
26
+ | "relation-members"
27
+ | "string-bytes"
28
+ | "total-way-refs"
29
+ | "total-relation-members"
30
+ | "total-string-bytes";
31
+
32
+ /** Thrown when an entity or dataset is larger than the storage layout can hold. */
33
+ export class OsmCapacityError extends Error {
34
+ readonly code = "OSM_CAPACITY_EXCEEDED";
35
+ readonly limit: OsmCapacityLimit;
36
+ readonly value: number;
37
+ readonly max: number;
38
+
39
+ constructor(limit: OsmCapacityLimit, value: number, max: number, subject: string) {
40
+ super(
41
+ `${subject} has ${value.toLocaleString("en-US")} ${capacityUnit(limit)}; ` +
42
+ `Osmix stores at most ${max.toLocaleString("en-US")}.`,
43
+ );
44
+ this.name = "OsmCapacityError";
45
+ this.limit = limit;
46
+ this.value = value;
47
+ this.max = max;
48
+ }
49
+ }
50
+
51
+ function capacityUnit(limit: OsmCapacityLimit): string {
52
+ switch (limit) {
53
+ case "way-refs":
54
+ case "total-way-refs":
55
+ return "node refs";
56
+ case "relation-members":
57
+ case "total-relation-members":
58
+ return "members";
59
+ case "string-bytes":
60
+ case "total-string-bytes":
61
+ return "UTF-8 bytes";
62
+ }
63
+ }
64
+
65
+ /** Throw {@link OsmCapacityError} when `value` is above `max`. */
66
+ export function assertCapacity(
67
+ limit: OsmCapacityLimit,
68
+ value: number,
69
+ max: number,
70
+ subject: () => string,
71
+ ): void {
72
+ if (value > max) throw new OsmCapacityError(limit, value, max, subject());
73
+ }
package/src/mocks.ts CHANGED
@@ -1,49 +1,50 @@
1
- import type { OsmNode } from "@osmix/shared/types"
2
- import { Osm } from "./osm.ts"
1
+ import type { OsmNode } from "@osmix/types";
3
2
 
4
- const YAKIM_LAT = 46.60207
5
- const YAKIM_LON = -120.505898
3
+ import { Osm } from "./osm.ts";
6
4
 
7
- const ONE_KM_LON = 0.0131 // approximately
8
- const ONE_KM_LAT = 0.009
5
+ const YAKIM_LAT = 46.60207;
6
+ const YAKIM_LON = -120.505898;
7
+
8
+ const ONE_KM_LON = 0.0131; // approximately
9
+ const ONE_KM_LAT = 0.009;
9
10
 
10
11
  const node0: OsmNode = {
11
- id: 0,
12
- lat: YAKIM_LAT,
13
- lon: YAKIM_LON,
14
- }
12
+ id: 0,
13
+ lat: YAKIM_LAT,
14
+ lon: YAKIM_LON,
15
+ };
15
16
 
16
17
  const node1: OsmNode = {
17
- id: 1,
18
- lat: YAKIM_LAT,
19
- lon: YAKIM_LON - ONE_KM_LON, // approximately 1km west
20
- }
18
+ id: 1,
19
+ lat: YAKIM_LAT,
20
+ lon: YAKIM_LON - ONE_KM_LON, // approximately 1km west
21
+ };
21
22
 
22
23
  function addBaseNodes(osm: Osm) {
23
- osm.nodes.addNode(node0)
24
- osm.nodes.addNode(node1)
24
+ osm.nodes.addNode(node0);
25
+ osm.nodes.addNode(node1);
25
26
  }
26
27
 
27
28
  function addBaseWays(osm: Osm) {
28
- osm.ways.addWay({
29
- id: 1,
30
- refs: [0, 1],
31
- tags: {
32
- highway: "primary",
33
- },
34
- })
29
+ osm.ways.addWay({
30
+ id: 1,
31
+ refs: [0, 1],
32
+ tags: {
33
+ highway: "primary",
34
+ },
35
+ });
35
36
  }
36
37
 
37
38
  /**
38
39
  * Create a base OSM with one way and two nodes.
39
40
  */
40
41
  export function createMockBaseOsm(): Osm {
41
- const base = new Osm()
42
- addBaseNodes(base)
43
- base.nodes.buildIndex()
44
- addBaseWays(base)
45
- base.buildIndexes()
46
- return base
42
+ const base = new Osm();
43
+ addBaseNodes(base);
44
+ base.nodes.buildIndex();
45
+ addBaseWays(base);
46
+ base.buildIndexes();
47
+ return base;
47
48
  }
48
49
 
49
50
  /**
@@ -53,88 +54,88 @@ export function createMockBaseOsm(): Osm {
53
54
  * - A way that crosses over the first way, but does not share a node and so starts disconnected
54
55
  */
55
56
  export function createMockPatchOsm(): Osm {
56
- const osm = new Osm()
57
-
58
- addBaseNodes(osm)
59
-
60
- // Add way the connects with base way node 0 (will replace it)
61
- osm.nodes.addNode({
62
- ...node0,
63
- id: 2,
64
- tags: {
65
- crossing: "yes",
66
- },
67
- })
68
- osm.nodes.addNode({
69
- ...node1,
70
- id: 3,
71
- lon: YAKIM_LON + ONE_KM_LON, // ends 1km east of the center point
72
- })
73
-
74
- // Add nodes for disconnected way
75
- osm.nodes.addNode({
76
- id: 4,
77
- lat: YAKIM_LAT - ONE_KM_LAT, // 1km south
78
- lon: YAKIM_LON + ONE_KM_LON / 4, // 250m east
79
- })
80
- osm.nodes.addNode({
81
- id: 5,
82
- lat: YAKIM_LAT + ONE_KM_LAT, // 1km north
83
- lon: YAKIM_LON + ONE_KM_LON / 4, // 250m east
84
- })
85
-
86
- // Add nodes for crossing way
87
- osm.nodes.addNode({
88
- id: 6,
89
- lat: YAKIM_LAT - ONE_KM_LAT, // 1km south
90
- lon: YAKIM_LON + ONE_KM_LON / 2, // 500m east
91
- })
92
- osm.nodes.addNode({
93
- id: 7,
94
- lat: YAKIM_LAT + ONE_KM_LAT, // 1km north
95
- lon: YAKIM_LON + ONE_KM_LON / 2, // 500m east
96
- })
97
-
98
- osm.nodes.buildIndex()
99
-
100
- // Add same base way with new tags
101
- osm.ways.addWay({
102
- id: 1,
103
- refs: [0, 1],
104
- tags: {
105
- highway: "primary",
106
- version: "2",
107
- },
108
- })
109
-
110
- // Add way that overlaps with base way node 1. Node should be de-duplicated.
111
- osm.ways.addWay({
112
- id: 2,
113
- refs: [2, 3],
114
- tags: {
115
- highway: "secondary",
116
- },
117
- })
118
-
119
- // Add way that crosses way 2, and should generate 1 intersection.
120
- osm.ways.addWay({
121
- id: 3,
122
- refs: [4, 5],
123
- tags: {
124
- highway: "primary",
125
- },
126
- })
127
-
128
- // Add a way that crosses way 2, but has a tag indicating it is an underpass and should be left alone
129
- osm.ways.addWay({
130
- id: 4,
131
- refs: [6, 7],
132
- tags: {
133
- highway: "underpass",
134
- tunnel: "yes",
135
- },
136
- })
137
-
138
- osm.buildIndexes()
139
- return osm
57
+ const osm = new Osm();
58
+
59
+ addBaseNodes(osm);
60
+
61
+ // Add way the connects with base way node 0 (will replace it)
62
+ osm.nodes.addNode({
63
+ ...node0,
64
+ id: 2,
65
+ tags: {
66
+ crossing: "yes",
67
+ },
68
+ });
69
+ osm.nodes.addNode({
70
+ ...node1,
71
+ id: 3,
72
+ lon: YAKIM_LON + ONE_KM_LON, // ends 1km east of the center point
73
+ });
74
+
75
+ // Add nodes for disconnected way
76
+ osm.nodes.addNode({
77
+ id: 4,
78
+ lat: YAKIM_LAT - ONE_KM_LAT, // 1km south
79
+ lon: YAKIM_LON + ONE_KM_LON / 4, // 250m east
80
+ });
81
+ osm.nodes.addNode({
82
+ id: 5,
83
+ lat: YAKIM_LAT + ONE_KM_LAT, // 1km north
84
+ lon: YAKIM_LON + ONE_KM_LON / 4, // 250m east
85
+ });
86
+
87
+ // Add nodes for crossing way
88
+ osm.nodes.addNode({
89
+ id: 6,
90
+ lat: YAKIM_LAT - ONE_KM_LAT, // 1km south
91
+ lon: YAKIM_LON + ONE_KM_LON / 2, // 500m east
92
+ });
93
+ osm.nodes.addNode({
94
+ id: 7,
95
+ lat: YAKIM_LAT + ONE_KM_LAT, // 1km north
96
+ lon: YAKIM_LON + ONE_KM_LON / 2, // 500m east
97
+ });
98
+
99
+ osm.nodes.buildIndex();
100
+
101
+ // Add same base way with new tags
102
+ osm.ways.addWay({
103
+ id: 1,
104
+ refs: [0, 1],
105
+ tags: {
106
+ highway: "primary",
107
+ version: "2",
108
+ },
109
+ });
110
+
111
+ // Add way that overlaps with base way node 1. Node should be de-duplicated.
112
+ osm.ways.addWay({
113
+ id: 2,
114
+ refs: [2, 3],
115
+ tags: {
116
+ highway: "secondary",
117
+ },
118
+ });
119
+
120
+ // Add way that crosses way 2, and should generate 1 intersection.
121
+ osm.ways.addWay({
122
+ id: 3,
123
+ refs: [4, 5],
124
+ tags: {
125
+ highway: "primary",
126
+ },
127
+ });
128
+
129
+ // Add a way that crosses way 2, but has a tag indicating it is an underpass and should be left alone
130
+ osm.ways.addWay({
131
+ id: 4,
132
+ refs: [6, 7],
133
+ tags: {
134
+ highway: "underpass",
135
+ tunnel: "yes",
136
+ },
137
+ });
138
+
139
+ osm.buildIndexes();
140
+ return osm;
140
141
  }