@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,80 @@
1
+ import { Osm } from "./osm.ts";
2
+
3
+ /** Old ID → new ID for each renumbered entity, by type. JSON- and structured-clone-safe. */
4
+ export interface OsmIdMap {
5
+ nodes: Record<number, number>;
6
+ ways: Record<number, number>;
7
+ relations: Record<number, number>;
8
+ }
9
+
10
+ /** New positive IDs for one type's negative IDs, after its highest positive ID, in −1, −2, … order. */
11
+ function positiveIds(ids: { size: number; at(index: number): number }): Map<number, number> {
12
+ let max = 0;
13
+ const negative: number[] = [];
14
+ for (let index = 0; index < ids.size; index++) {
15
+ const id = ids.at(index);
16
+ if (id < 0) negative.push(id);
17
+ else max = Math.max(max, id);
18
+ }
19
+ negative.sort((a, b) => b - a);
20
+ return new Map(negative.map((id, index) => [id, max + index + 1]));
21
+ }
22
+
23
+ /**
24
+ * A copy of `osm` whose negative node, way and relation IDs are replaced by positive ones, for
25
+ * tools that reject negative IDs. In OSM files a negative ID conventionally marks a new, not yet
26
+ * uploaded entity, so only do this when the consumer needs it. Each type gets IDs after its own
27
+ * highest positive ID, in −1, −2, … order, so the result is deterministic. Way refs and relation
28
+ * members follow. `osm` is unchanged; the copy has its indexes built.
29
+ */
30
+ export function renumberNegativeIds(osm: Osm, options: { id?: string } = {}) {
31
+ const nodeIds = positiveIds(osm.nodes.ids);
32
+ const wayIds = positiveIds(osm.ways.ids);
33
+ const relationIds = positiveIds(osm.relations.ids);
34
+ const mapped = { node: nodeIds, way: wayIds, relation: relationIds } as const;
35
+ const renumbered = new Osm({ id: options.id ?? osm.id, header: osm.header });
36
+ for (const node of osm.nodes.sorted()) {
37
+ renumbered.nodes.addNode({ ...node, id: nodeIds.get(node.id) ?? node.id });
38
+ }
39
+ for (const way of osm.ways.sorted()) {
40
+ renumbered.ways.addWay({
41
+ ...way,
42
+ id: wayIds.get(way.id) ?? way.id,
43
+ refs: way.refs.map((ref) => nodeIds.get(ref) ?? ref),
44
+ });
45
+ }
46
+ for (const relation of osm.relations.sorted()) {
47
+ renumbered.relations.addRelation({
48
+ ...relation,
49
+ id: relationIds.get(relation.id) ?? relation.id,
50
+ members: relation.members.map((member) => ({
51
+ ...member,
52
+ ref: mapped[member.type].get(member.ref) ?? member.ref,
53
+ })),
54
+ });
55
+ }
56
+ renumbered.buildIndexes();
57
+ renumbered.buildSpatialIndexes();
58
+ return { osm: renumbered, idMap: toIdMap(nodeIds, wayIds, relationIds) };
59
+ }
60
+
61
+ function toIdMap(
62
+ nodes: Map<number, number>,
63
+ ways: Map<number, number>,
64
+ relations: Map<number, number>,
65
+ ): OsmIdMap {
66
+ return {
67
+ nodes: Object.fromEntries(nodes),
68
+ ways: Object.fromEntries(ways),
69
+ relations: Object.fromEntries(relations),
70
+ };
71
+ }
72
+
73
+ /** The ID map `renumberNegativeIds(osm)` would apply, without copying the data. */
74
+ export function negativeIdMap(osm: Osm): OsmIdMap {
75
+ return toIdMap(
76
+ positiveIds(osm.nodes.ids),
77
+ positiveIds(osm.ways.ids),
78
+ positiveIds(osm.relations.ids),
79
+ );
80
+ }
@@ -7,201 +7,206 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import type { OsmPbfStringTable } from "@osmix/pbf"
11
- import type { ContentHasher } from "@osmix/shared/content-hasher"
12
- import { type BufferType, ResizeableTypedArray as RTA } from "./typed-arrays.ts"
10
+ import type { OsmPbfStringTable } from "@osmix/pbf";
11
+ import type { ContentHasher } from "@osmix/shared/content-hasher";
12
+
13
+ import { MAX_STRING_BYTES, MAX_UINT32_OFFSET, assertCapacity } from "./limits.ts";
14
+ import { type BufferType, ResizeableTypedArray as RTA } from "./typed-arrays.ts";
13
15
 
14
16
  /**
15
17
  * Serializable state for worker transfer.
16
18
  */
17
19
  export interface StringTableTransferables<T extends BufferType = BufferType> {
18
- /** Concatenated UTF-8 bytes. */
19
- bytes: T
20
- /** Maps string index → byte offset. */
21
- start: T
22
- /** Maps string index → byte length. */
23
- count: T
20
+ /** Concatenated UTF-8 bytes. */
21
+ bytes: T;
22
+ /** Maps string index → byte offset. */
23
+ start: T;
24
+ /** Maps string index → byte length. */
25
+ count: T;
24
26
  }
25
27
 
26
28
  /**
27
29
  * Append-only deduplicated string table.
28
30
  *
29
- * Limits: Max string length 65,535 bytes.
31
+ * Limits: a string holds at most `MAX_STRING_BYTES` (65,535) UTF-8 bytes, and the table
32
+ * holds at most `MAX_UINT32_OFFSET` bytes in total. Larger input throws `OsmCapacityError`.
30
33
  * Rebuilds reverse index lazily after transfer.
31
34
  */
32
35
  export default class StringTable {
33
- /** UTF-8 encoder for string→bytes conversion */
34
- private enc = new TextEncoder()
35
- /** UTF-8 decoder for bytes→string conversion */
36
- private dec = new TextDecoder() // UTF-8
37
-
38
- // ─── Serializable State ────────────────────────────────────────────────────
39
- /** Concatenated UTF-8 bytes of all strings */
40
- bytes: RTA<Uint8Array>
41
- /** Maps string index → byte offset in bytes array */
42
- start: RTA<Uint32Array>
43
- /** Maps string index → byte length */
44
- count: RTA<Uint16Array>
45
-
46
- // ─── Builder State (string → index) ────────────────────────────────────────
47
- /** Forward lookup: string → index (populated during add) */
48
- private stringToIndex = new Map<string, number>()
49
-
50
- /** Whether the reverse index has been built (lazy after transfer) */
51
- private reverseIndexBuilt = false
52
-
53
- // ─── Retrieval Cache (index → string) ──────────────────────────────────────
54
- /** Cache of decoded strings to avoid repeated UTF-8 decoding */
55
- private indexToString = new Map<number, string>()
56
-
57
- /**
58
- * Create a new StringTable.
59
- */
60
- constructor(opts?: StringTableTransferables) {
61
- this.bytes = opts?.bytes
62
- ? RTA.from(Uint8Array, opts.bytes)
63
- : new RTA(Uint8Array)
64
- this.start = opts?.start
65
- ? RTA.from(Uint32Array, opts.start)
66
- : new RTA(Uint32Array)
67
- this.count = opts?.count
68
- ? RTA.from(Uint16Array, opts.count)
69
- : new RTA(Uint16Array)
70
- }
71
-
72
- /**
73
- * Get transferable objects for passing to another thread.
74
- */
75
- transferables(): StringTableTransferables {
76
- return {
77
- bytes: this.bytes.array.buffer,
78
- start: this.start.array.buffer,
79
- count: this.count.array.buffer,
80
- }
81
- }
82
-
83
- /**
84
- * Add a string to the table and return its index.
85
- */
86
- add(str: string): number {
87
- const existingIndex = this.stringToIndex.get(str)
88
- if (existingIndex !== undefined) return existingIndex
89
- const startIndex = this.start.length
90
- const encoded = this.enc.encode(str)
91
- this.start.push(this.bytes.length)
92
- this.bytes.pushMany(encoded)
93
- this.count.push(encoded.length)
94
- this.stringToIndex.set(str, startIndex)
95
- return startIndex
96
- }
97
-
98
- /**
99
- * Decode all the strings in a primitive block and add them to the string table.
100
- * Return a mapping of block index -> string table index
101
- */
102
- createBlockIndexMap(blockStringtable: OsmPbfStringTable) {
103
- const index = new Uint32Array(blockStringtable.length)
104
- for (let i = 0; i < blockStringtable.length; i++) {
105
- const bytesString = blockStringtable[i]
106
- const str = this.dec.decode(bytesString)
107
- const existingIndex = this.stringToIndex.get(str)
108
- if (existingIndex !== undefined) {
109
- index[i] = existingIndex
110
- continue
111
- }
112
- index[i] = this.add(str)
113
- }
114
- return index
115
- }
116
-
117
- /**
118
- * Get a string by its index.
119
- * Caches results to avoid repeated UTF-8 decoding.
120
- */
121
- get(index: number): string {
122
- const string = this.indexToString.get(index)
123
- if (string) return string
124
- const bytes = this.getBytes(index)
125
- const tempBuffer = new ArrayBuffer(bytes.byteLength)
126
- const tempBytes = new Uint8Array(tempBuffer)
127
- tempBytes.set(bytes)
128
- const decoded = this.dec.decode(tempBytes)
129
- this.indexToString.set(index, decoded)
130
- return decoded
131
- }
132
-
133
- /**
134
- * Get the raw UTF-8 bytes of a string.
135
- * Returns a subarray view (not a copy).
136
- */
137
- getBytes(index: number): Uint8Array {
138
- if (index < 0 || index >= this.length)
139
- throw Error(`String index out of range: ${index}`)
140
- const start = this.start.at(index)
141
- const count = this.count.at(index)
142
- return this.bytes.array.subarray(start, start + count)
143
- }
144
-
145
- /** Number of strings in the table. */
146
- get length() {
147
- return this.start.length
148
- }
149
-
150
- /**
151
- * Finalize the string table by compacting internal arrays.
152
- *
153
- * This releases unused buffer capacity and marks the reverse index as built
154
- * (since all strings were added before this call).
155
- */
156
- buildIndex() {
157
- this.bytes.compact()
158
- this.start.compact()
159
- this.count.compact()
160
- this.reverseIndexBuilt = true
161
- }
162
-
163
- /**
164
- * Convert the string table to an OSM PBF string table.
165
- */
166
- toOsmPbfStringTable(): OsmPbfStringTable {
167
- const stringTable: OsmPbfStringTable = []
168
- for (let i = 0; i < this.length; i++) {
169
- stringTable.push(this.getBytes(i))
170
- }
171
- return stringTable
172
- }
173
-
174
- /**
175
- * Lazily build the reverse index (string → index).
176
- * Decodes all strings once to populate the lookup map.
177
- */
178
- private ensureReverseIndex() {
179
- if (this.reverseIndexBuilt) return
180
- // Decode all strings and populate the forward lookup map
181
- for (let i = 0; i < this.length; i++) {
182
- this.stringToIndex.set(this.get(i), i)
183
- }
184
- this.reverseIndexBuilt = true
185
- }
186
-
187
- /**
188
- * Find the index of a string.
189
- */
190
- find(str: string): number {
191
- const existing = this.stringToIndex.get(str)
192
- if (existing !== undefined) return existing
193
- this.ensureReverseIndex()
194
- return this.stringToIndex.get(str) ?? -1
195
- }
196
-
197
- /**
198
- * Update a ContentHasher with the string table's data.
199
- * Hashes the bytes, start offsets, and counts.
200
- */
201
- updateHash(hasher: ContentHasher): ContentHasher {
202
- return hasher
203
- .update(this.bytes.array)
204
- .update(this.start.array)
205
- .update(this.count.array)
206
- }
36
+ /** UTF-8 encoder for string→bytes conversion */
37
+ private enc = new TextEncoder();
38
+ /** UTF-8 decoder for bytes→string conversion */
39
+ private dec = new TextDecoder(); // UTF-8
40
+
41
+ // ─── Serializable State ────────────────────────────────────────────────────
42
+ /** Concatenated UTF-8 bytes of all strings */
43
+ bytes: RTA<Uint8Array>;
44
+ /** Maps string index → byte offset in bytes array */
45
+ start: RTA<Uint32Array>;
46
+ /** Maps string index → byte length */
47
+ count: RTA<Uint16Array>;
48
+
49
+ // ─── Builder State (string → index) ────────────────────────────────────────
50
+ /** Forward lookup: string → index (populated during add) */
51
+ private stringToIndex = new Map<string, number>();
52
+
53
+ /** Whether the reverse index has been built (lazy after transfer) */
54
+ private reverseIndexBuilt = false;
55
+
56
+ // ─── Retrieval Cache (index → string) ──────────────────────────────────────
57
+ /** Cache of decoded strings to avoid repeated UTF-8 decoding */
58
+ private indexToString = new Map<number, string>();
59
+
60
+ /**
61
+ * Create a new StringTable.
62
+ */
63
+ constructor(opts?: StringTableTransferables) {
64
+ this.bytes = opts?.bytes ? RTA.from(Uint8Array, opts.bytes) : new RTA(Uint8Array);
65
+ this.start = opts?.start ? RTA.from(Uint32Array, opts.start) : new RTA(Uint32Array);
66
+ this.count = opts?.count ? RTA.from(Uint16Array, opts.count) : new RTA(Uint16Array);
67
+ }
68
+
69
+ /**
70
+ * Get transferable objects for passing to another thread.
71
+ */
72
+ transferables(): StringTableTransferables {
73
+ return {
74
+ bytes: this.bytes.array.buffer,
75
+ start: this.start.array.buffer,
76
+ count: this.count.array.buffer,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Add a string to the table and return its index.
82
+ */
83
+ add(str: string): number {
84
+ const existingIndex = this.stringToIndex.get(str);
85
+ if (existingIndex !== undefined) return existingIndex;
86
+ const startIndex = this.start.length;
87
+ const encoded = this.enc.encode(str);
88
+ assertCapacity(
89
+ "string-bytes",
90
+ encoded.length,
91
+ MAX_STRING_BYTES,
92
+ () => `String "${str.slice(0, 40)}…"`,
93
+ );
94
+ assertCapacity(
95
+ "total-string-bytes",
96
+ this.bytes.length + encoded.length,
97
+ MAX_UINT32_OFFSET,
98
+ () => "The string table",
99
+ );
100
+ this.start.push(this.bytes.length);
101
+ this.bytes.pushMany(encoded);
102
+ this.count.push(encoded.length);
103
+ this.stringToIndex.set(str, startIndex);
104
+ return startIndex;
105
+ }
106
+
107
+ /**
108
+ * Decode all the strings in a primitive block and add them to the string table.
109
+ * Return a mapping of block index -> string table index
110
+ */
111
+ createBlockIndexMap(blockStringtable: OsmPbfStringTable) {
112
+ const index = new Uint32Array(blockStringtable.length);
113
+ for (let i = 0; i < blockStringtable.length; i++) {
114
+ const bytesString = blockStringtable[i];
115
+ const str = this.dec.decode(bytesString);
116
+ const existingIndex = this.stringToIndex.get(str);
117
+ if (existingIndex !== undefined) {
118
+ index[i] = existingIndex;
119
+ continue;
120
+ }
121
+ index[i] = this.add(str);
122
+ }
123
+ return index;
124
+ }
125
+
126
+ /**
127
+ * Get a string by its index.
128
+ * Caches results to avoid repeated UTF-8 decoding.
129
+ */
130
+ get(index: number): string {
131
+ const string = this.indexToString.get(index);
132
+ if (string) return string;
133
+ const bytes = this.getBytes(index);
134
+ const tempBuffer = new ArrayBuffer(bytes.byteLength);
135
+ const tempBytes = new Uint8Array(tempBuffer);
136
+ tempBytes.set(bytes);
137
+ const decoded = this.dec.decode(tempBytes);
138
+ this.indexToString.set(index, decoded);
139
+ return decoded;
140
+ }
141
+
142
+ /**
143
+ * Get the raw UTF-8 bytes of a string.
144
+ * Returns a subarray view (not a copy).
145
+ */
146
+ getBytes(index: number): Uint8Array {
147
+ if (index < 0 || index >= this.length) throw Error(`String index out of range: ${index}`);
148
+ const start = this.start.at(index);
149
+ const count = this.count.at(index);
150
+ return this.bytes.array.subarray(start, start + count);
151
+ }
152
+
153
+ /** Number of strings in the table. */
154
+ get length() {
155
+ return this.start.length;
156
+ }
157
+
158
+ /**
159
+ * Finalize the string table by compacting internal arrays.
160
+ *
161
+ * This releases unused buffer capacity and marks the reverse index as built
162
+ * (since all strings were added before this call).
163
+ */
164
+ buildIndex() {
165
+ this.bytes.compact();
166
+ this.start.compact();
167
+ this.count.compact();
168
+ this.reverseIndexBuilt = true;
169
+ }
170
+
171
+ /**
172
+ * Convert the string table to an OSM PBF string table.
173
+ */
174
+ toOsmPbfStringTable(): OsmPbfStringTable {
175
+ const stringTable: OsmPbfStringTable = [];
176
+ for (let i = 0; i < this.length; i++) {
177
+ stringTable.push(this.getBytes(i));
178
+ }
179
+ return stringTable;
180
+ }
181
+
182
+ /**
183
+ * Lazily build the reverse index (string → index).
184
+ * Decodes all strings once to populate the lookup map.
185
+ */
186
+ private ensureReverseIndex() {
187
+ if (this.reverseIndexBuilt) return;
188
+ // Decode all strings and populate the forward lookup map
189
+ for (let i = 0; i < this.length; i++) {
190
+ this.stringToIndex.set(this.get(i), i);
191
+ }
192
+ this.reverseIndexBuilt = true;
193
+ }
194
+
195
+ /**
196
+ * Find the index of a string.
197
+ */
198
+ find(str: string): number {
199
+ const existing = this.stringToIndex.get(str);
200
+ if (existing !== undefined) return existing;
201
+ this.ensureReverseIndex();
202
+ return this.stringToIndex.get(str) ?? -1;
203
+ }
204
+
205
+ /**
206
+ * Update a ContentHasher with the string table's data.
207
+ * Hashes the bytes, start offsets, and counts.
208
+ */
209
+ updateHash(hasher: ContentHasher): ContentHasher {
210
+ return hasher.update(this.bytes.array).update(this.start.array).update(this.count.array);
211
+ }
207
212
  }