@osmix/core 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.
Files changed (87) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +54 -7
  3. package/dist/contracts.d.ts +2 -1
  4. package/dist/contracts.d.ts.map +1 -1
  5. package/dist/entities.d.ts +21 -1
  6. package/dist/entities.d.ts.map +1 -1
  7. package/dist/entities.js +52 -3
  8. package/dist/entities.js.map +1 -1
  9. package/dist/ids.d.ts +18 -5
  10. package/dist/ids.d.ts.map +1 -1
  11. package/dist/ids.js +68 -18
  12. package/dist/ids.js.map +1 -1
  13. package/dist/index.d.ts +6 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/indirect-kd-index.d.ts +28 -0
  18. package/dist/indirect-kd-index.d.ts.map +1 -0
  19. package/dist/indirect-kd-index.js +163 -0
  20. package/dist/indirect-kd-index.js.map +1 -0
  21. package/dist/limits.d.ts +30 -0
  22. package/dist/limits.d.ts.map +1 -0
  23. package/dist/limits.js +51 -0
  24. package/dist/limits.js.map +1 -0
  25. package/dist/nodes.d.ts +28 -13
  26. package/dist/nodes.d.ts.map +1 -1
  27. package/dist/nodes.js +181 -51
  28. package/dist/nodes.js.map +1 -1
  29. package/dist/osm.d.ts +59 -3
  30. package/dist/osm.d.ts.map +1 -1
  31. package/dist/osm.js +31 -2
  32. package/dist/osm.js.map +1 -1
  33. package/dist/relations.d.ts +10 -4
  34. package/dist/relations.d.ts.map +1 -1
  35. package/dist/relations.js +66 -28
  36. package/dist/relations.js.map +1 -1
  37. package/dist/renumber.d.ts +23 -0
  38. package/dist/renumber.d.ts.map +1 -0
  39. package/dist/renumber.js +64 -0
  40. package/dist/renumber.js.map +1 -0
  41. package/dist/stringtable.d.ts +2 -1
  42. package/dist/stringtable.d.ts.map +1 -1
  43. package/dist/stringtable.js +5 -1
  44. package/dist/stringtable.js.map +1 -1
  45. package/dist/tags.d.ts +44 -11
  46. package/dist/tags.d.ts.map +1 -1
  47. package/dist/tags.js +153 -27
  48. package/dist/tags.js.map +1 -1
  49. package/dist/typed-arrays.d.ts +63 -48
  50. package/dist/typed-arrays.d.ts.map +1 -1
  51. package/dist/typed-arrays.js +184 -107
  52. package/dist/typed-arrays.js.map +1 -1
  53. package/dist/ways.d.ts +50 -2
  54. package/dist/ways.d.ts.map +1 -1
  55. package/dist/ways.js +212 -35
  56. package/dist/ways.js.map +1 -1
  57. package/package.json +11 -12
  58. package/src/contracts.ts +2 -1
  59. package/src/entities.ts +67 -4
  60. package/src/ids.ts +72 -22
  61. package/src/index.ts +13 -7
  62. package/src/indirect-kd-index.ts +212 -0
  63. package/src/limits.ts +73 -0
  64. package/src/nodes.ts +228 -54
  65. package/src/osm.ts +90 -4
  66. package/src/relations.ts +80 -30
  67. package/src/renumber.ts +80 -0
  68. package/src/stringtable.ts +15 -1
  69. package/src/tags.ts +173 -31
  70. package/src/typed-arrays.ts +225 -113
  71. package/src/ways.ts +233 -38
  72. package/test/entity-index-errors.test.ts +41 -0
  73. package/test/ids.bench.ts +11 -7
  74. package/test/ids.test.ts +38 -0
  75. package/test/limits.test.ts +83 -0
  76. package/test/no-shared-array-buffer.test.ts +41 -0
  77. package/test/nodes-spatial-index.test.ts +261 -0
  78. package/test/osm-bbox.test.ts +30 -0
  79. package/test/relations.bench.ts +9 -4
  80. package/test/relations.test.ts +52 -29
  81. package/test/renumber.test.ts +54 -0
  82. package/test/spatial-index-rebuild.test.ts +33 -0
  83. package/test/storage-compaction.test.ts +269 -0
  84. package/test/tags-search.test.ts +13 -0
  85. package/test/typed-arrays.test.ts +178 -59
  86. package/src/types/geokdbush.d.ts +0 -32
  87. package/src/types/kdbush.d.ts +0 -47
package/src/ids.ts CHANGED
@@ -30,10 +30,10 @@ const BLOCK_SIZE = 256;
30
30
  export interface IdsTransferables<T extends BufferType = BufferType> {
31
31
  /** Entity IDs in insertion order. */
32
32
  ids: T;
33
- /** Sorted IDs for binary search (aliases `ids` if already sorted). */
34
- sortedIds: T;
35
- /** Maps sorted position → original insertion index. */
36
- sortedIdPositionToIndex: T;
33
+ /** Sorted IDs for binary search. Omitted when `idsAreSorted` is true. */
34
+ sortedIds?: T;
35
+ /** Maps sorted position → original insertion index. Omitted for sorted IDs. */
36
+ sortedIdPositionToIndex?: T;
37
37
  /** Anchor array: every BLOCK_SIZE-th sorted ID. */
38
38
  anchors: T;
39
39
  /** True if IDs were inserted in ascending order. */
@@ -67,8 +67,16 @@ export class Ids {
67
67
  constructor(transferables?: IdsTransferables) {
68
68
  if (transferables) {
69
69
  this.ids = RTA.from(Float64Array, transferables.ids);
70
- this.idsSorted = new Float64Array(transferables.sortedIds);
71
- this.sortedIdPositionToIndex = new Uint32Array(transferables.sortedIdPositionToIndex);
70
+ if (transferables.idsAreSorted) {
71
+ this.idsSorted = this.ids.array;
72
+ this.sortedIdPositionToIndex = new Uint32Array(new BufferConstructor(0));
73
+ } else {
74
+ if (!transferables.sortedIds || !transferables.sortedIdPositionToIndex) {
75
+ throw Error("Unsorted IDs require sorted lookup buffers.");
76
+ }
77
+ this.idsSorted = new Float64Array(transferables.sortedIds);
78
+ this.sortedIdPositionToIndex = new Uint32Array(transferables.sortedIdPositionToIndex);
79
+ }
72
80
  this.anchors = new Float64Array(transferables.anchors);
73
81
  this.idsAreSorted = transferables.idsAreSorted;
74
82
  this.indexBuilt = true;
@@ -157,12 +165,8 @@ export class Ids {
157
165
  } else {
158
166
  // Point to the same array
159
167
  this.idsSorted = this.ids.array;
160
- // Create the sortedIdPositionToIndex array
161
- const posBuffer = new BufferConstructor(this.size * Uint32Array.BYTES_PER_ELEMENT);
162
- this.sortedIdPositionToIndex = new Uint32Array(posBuffer);
163
- for (let i = 0; i < this.size; i++) {
164
- this.sortedIdPositionToIndex[i] = i;
165
- }
168
+ // Sorted position is already the insertion index; no identity map is needed.
169
+ this.sortedIdPositionToIndex = new Uint32Array(new BufferConstructor(0));
166
170
  }
167
171
 
168
172
  // Build anchors (every blockSize-th key)
@@ -238,37 +242,83 @@ export class Ids {
238
242
  /** @internal Iterate sorted IDs with their original storage positions. */
239
243
  *sortedEntries(): Generator<readonly [id: number, index: number]> {
240
244
  for (let i = 0; i < this.idsSorted.length; i++) {
241
- const id = this.idsSorted[i];
242
- const index = this.sortedIdPositionToIndex[i];
245
+ yield this.sortedEntry(i);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Iterate in canonical OSM file order: negative IDs first by increasing
251
+ * absolute value, followed by non-negative IDs in ascending order.
252
+ *
253
+ * @internal
254
+ */
255
+ *osmSortedEntries(): Generator<readonly [id: number, index: number]> {
256
+ let firstNonNegative = 0;
257
+ while (firstNonNegative < this.idsSorted.length && this.idsSorted[firstNonNegative]! < 0) {
258
+ firstNonNegative++;
259
+ }
260
+
261
+ // Numeric sorting places negative IDs in the opposite of canonical OSM
262
+ // order. Reverse ID groups while preserving duplicate insertion order.
263
+ let groupEnd = firstNonNegative;
264
+ while (groupEnd > 0) {
265
+ const id = this.idsSorted[groupEnd - 1];
243
266
  assertValue(id, "Sorted ID is undefined");
244
- assertValue(index, "Sorted position is undefined");
245
- yield [id, index];
267
+ let groupStart = groupEnd - 1;
268
+ while (groupStart > 0 && this.idsSorted[groupStart - 1] === id) groupStart--;
269
+ for (let position = groupStart; position < groupEnd; position++) {
270
+ yield this.sortedEntry(position);
271
+ }
272
+ groupEnd = groupStart;
273
+ }
274
+
275
+ for (let position = firstNonNegative; position < this.idsSorted.length; position++) {
276
+ yield this.sortedEntry(position);
246
277
  }
247
278
  }
248
279
 
280
+ private sortedEntry(position: number): readonly [id: number, index: number] {
281
+ const id = this.idsSorted[position];
282
+ assertValue(id, "Sorted ID is undefined");
283
+ if (this.idsAreSorted) return [id, position];
284
+ const index = this.sortedIdPositionToIndex[position];
285
+ assertValue(index, "Sorted position is undefined");
286
+ return [id, index];
287
+ }
288
+
249
289
  /**
250
290
  * Get transferable buffers for passing to another thread.
251
291
  * @returns Serializable representation of this index.
252
292
  */
253
293
  transferables(): IdsTransferables {
254
- return {
294
+ const base: IdsTransferables = {
255
295
  ids: this.ids.array.buffer,
256
- sortedIds: this.idsSorted.buffer,
257
- sortedIdPositionToIndex: this.sortedIdPositionToIndex.buffer,
258
296
  anchors: this.anchors.buffer,
259
297
  idsAreSorted: this.idsAreSorted,
260
298
  };
299
+ if (this.idsAreSorted) return base;
300
+ return {
301
+ ...base,
302
+ sortedIds: this.idsSorted.buffer,
303
+ sortedIdPositionToIndex: this.sortedIdPositionToIndex.buffer,
304
+ };
261
305
  }
262
306
 
263
307
  /**
264
308
  * Get the approximate memory requirements for a given number of IDs in bytes.
309
+ *
310
+ * `idsAreSorted` defaults to true because PBF entity collections arrive in
311
+ * ascending ID order and retain no separate sort buffers. Pass false when
312
+ * estimating a programmatically built (unsorted) collection, which also
313
+ * retains sorted IDs plus a position map.
265
314
  */
266
- static getBytesRequired(count: number) {
315
+ static getBytesRequired(count: number, idsAreSorted = true) {
267
316
  if (count === 0) return 0;
268
317
  return (
269
318
  count * Float64Array.BYTES_PER_ELEMENT + // ids
270
- count * Float64Array.BYTES_PER_ELEMENT + // sortedIds
271
- count * Uint32Array.BYTES_PER_ELEMENT + // sortedIdPositionToIndex
319
+ (idsAreSorted
320
+ ? 0
321
+ : count * (Float64Array.BYTES_PER_ELEMENT + Uint32Array.BYTES_PER_ELEMENT)) + // sort index
272
322
  Math.ceil(count / BLOCK_SIZE) * Float64Array.BYTES_PER_ELEMENT // anchors
273
323
  );
274
324
  }
package/src/index.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Efficiently stores and queries OpenStreetMap entities (nodes, ways, relations).
5
5
  *
6
6
  * Features:
7
- * - **Memory-efficient**: Uses typed arrays and Int32Array microdegrees.
8
- * - **Spatial indexing**: KDBush (points) and Flatbush (bboxes) for fast geographic queries.
7
+ * - **Memory-efficient**: Uses typed arrays; coordinates are Int32 at 1e-7 degrees.
8
+ * - **Spatial indexing**: Compact indirect KD trees (points) and Flatbush (bboxes).
9
9
  * - **Worker-ready**: Zero-copy transfer via `transferables()`.
10
10
  * - **Tag indexing**: Fast reverse lookup by tag key.
11
11
  *
@@ -22,19 +22,25 @@
22
22
  * @module @osmix/core
23
23
  */
24
24
 
25
- // oxlint-disable-next-line typescript/triple-slash-reference -- ambient module declarations for untyped packages
26
- /// <reference path="./types/kdbush.d.ts" />
27
- // oxlint-disable-next-line typescript/triple-slash-reference -- ambient module declarations for untyped packages
28
- /// <reference path="./types/geokdbush.d.ts" />
29
25
  // oxlint-disable-next-line typescript/triple-slash-reference -- ambient module declarations for untyped packages
30
26
  /// <reference path="./types/geoflatbush.d.ts" />
31
27
 
32
28
  export type { IdOrIndex } from "./ids.ts";
33
29
  export type { OsmReader, OsmWriter } from "./contracts.ts";
30
+ export { OsmEntityIndexBuildError, type OsmEntityIndexComponent } from "./entities.ts";
31
+ export * from "./limits.ts";
34
32
  export * from "./nodes.ts";
35
33
  export * from "./osm.ts";
36
34
  export * from "./relations.ts";
35
+ export { negativeIdMap, renumberNegativeIds, type OsmIdMap } from "./renumber.ts";
37
36
  export * from "./stringtable.ts";
38
37
  export * from "./tags.ts";
39
- export { BufferConstructor, type BufferType } from "./typed-arrays.ts";
38
+ export {
39
+ BufferConstructor,
40
+ ResizeableTypedArray,
41
+ TypedBufferAllocationError,
42
+ type BufferType,
43
+ type TypedBufferAllocationOperation,
44
+ type TypedBufferType,
45
+ } from "./typed-arrays.ts";
40
46
  export * from "./ways.ts";
@@ -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
+ }