@osmix/core 0.0.2 → 0.1.1

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 (104) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +118 -107
  3. package/dist/src/entities.d.ts +103 -9
  4. package/dist/src/entities.d.ts.map +1 -1
  5. package/dist/src/entities.js +101 -11
  6. package/dist/src/entities.js.map +1 -1
  7. package/dist/src/ids.d.ts +82 -13
  8. package/dist/src/ids.d.ts.map +1 -1
  9. package/dist/src/ids.js +103 -31
  10. package/dist/src/ids.js.map +1 -1
  11. package/dist/src/index.d.ts +31 -5
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +33 -5
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/nodes.d.ts +91 -21
  16. package/dist/src/nodes.d.ts.map +1 -1
  17. package/dist/src/nodes.js +191 -44
  18. package/dist/src/nodes.js.map +1 -1
  19. package/dist/src/osm.d.ts +99 -0
  20. package/dist/src/osm.d.ts.map +1 -0
  21. package/dist/src/osm.js +165 -0
  22. package/dist/src/osm.js.map +1 -0
  23. package/dist/src/relations.d.ts +112 -20
  24. package/dist/src/relations.d.ts.map +1 -1
  25. package/dist/src/relations.js +298 -35
  26. package/dist/src/relations.js.map +1 -1
  27. package/dist/src/stringtable.d.ts +78 -14
  28. package/dist/src/stringtable.d.ts.map +1 -1
  29. package/dist/src/stringtable.js +105 -26
  30. package/dist/src/stringtable.js.map +1 -1
  31. package/dist/src/tags.d.ts +117 -18
  32. package/dist/src/tags.d.ts.map +1 -1
  33. package/dist/src/tags.js +177 -30
  34. package/dist/src/tags.js.map +1 -1
  35. package/dist/src/typed-arrays.d.ts +85 -11
  36. package/dist/src/typed-arrays.d.ts.map +1 -1
  37. package/dist/src/typed-arrays.js +88 -9
  38. package/dist/src/typed-arrays.js.map +1 -1
  39. package/dist/src/utils.d.ts +35 -1
  40. package/dist/src/utils.d.ts.map +1 -1
  41. package/dist/src/utils.js +69 -10
  42. package/dist/src/utils.js.map +1 -1
  43. package/dist/src/ways.d.ts +91 -27
  44. package/dist/src/ways.d.ts.map +1 -1
  45. package/dist/src/ways.js +229 -69
  46. package/dist/src/ways.js.map +1 -1
  47. package/dist/test/memory.test.d.ts +7 -0
  48. package/dist/test/memory.test.d.ts.map +1 -0
  49. package/dist/test/memory.test.js +29 -0
  50. package/dist/test/memory.test.js.map +1 -0
  51. package/dist/test/mock-osm.d.ts +3 -3
  52. package/dist/test/mock-osm.d.ts.map +1 -1
  53. package/dist/test/mock-osm.js +3 -3
  54. package/dist/test/mock-osm.js.map +1 -1
  55. package/dist/test/tags-search.test.d.ts +2 -0
  56. package/dist/test/tags-search.test.d.ts.map +1 -0
  57. package/dist/test/tags-search.test.js +56 -0
  58. package/dist/test/tags-search.test.js.map +1 -0
  59. package/dist/test/typed-arrays.test.js +19 -17
  60. package/dist/test/typed-arrays.test.js.map +1 -1
  61. package/package.json +11 -13
  62. package/src/entities.ts +147 -22
  63. package/src/ids.ts +126 -45
  64. package/src/index.ts +34 -5
  65. package/src/nodes.ts +248 -58
  66. package/src/osm.ts +222 -0
  67. package/src/relations.ts +396 -54
  68. package/src/stringtable.ts +123 -36
  69. package/src/tags.ts +220 -46
  70. package/src/typed-arrays.ts +97 -10
  71. package/src/types/geoflatbush.d.ts +37 -0
  72. package/src/types/geokdbush.d.ts +37 -0
  73. package/src/types/kdbush.d.ts +24 -6
  74. package/src/utils.ts +80 -14
  75. package/src/ways.ts +260 -84
  76. package/test/memory.test.ts +38 -0
  77. package/test/mock-osm.ts +8 -8
  78. package/test/tags-search.test.ts +60 -0
  79. package/test/typed-arrays.test.ts +5 -6
  80. package/dist/src/osmix.d.ts +0 -88
  81. package/dist/src/osmix.d.ts.map +0 -1
  82. package/dist/src/osmix.js +0 -438
  83. package/dist/src/osmix.js.map +0 -1
  84. package/dist/test/extract.bench.d.ts +0 -2
  85. package/dist/test/extract.bench.d.ts.map +0 -1
  86. package/dist/test/extract.bench.js +0 -23
  87. package/dist/test/extract.bench.js.map +0 -1
  88. package/dist/test/extract.test.d.ts +0 -2
  89. package/dist/test/extract.test.d.ts.map +0 -1
  90. package/dist/test/extract.test.js +0 -127
  91. package/dist/test/extract.test.js.map +0 -1
  92. package/dist/test/read.test.d.ts +0 -2
  93. package/dist/test/read.test.d.ts.map +0 -1
  94. package/dist/test/read.test.js +0 -17
  95. package/dist/test/read.test.js.map +0 -1
  96. package/dist/test/write.test.d.ts +0 -2
  97. package/dist/test/write.test.d.ts.map +0 -1
  98. package/dist/test/write.test.js +0 -55
  99. package/dist/test/write.test.js.map +0 -1
  100. package/src/osmix.ts +0 -556
  101. package/test/extract.bench.ts +0 -29
  102. package/test/extract.test.ts +0 -161
  103. package/test/read.test.ts +0 -22
  104. package/test/write.test.ts +0 -69
@@ -1,44 +1,77 @@
1
- import type { OsmPbfBlock, OsmPbfStringTable } from "@osmix/pbf"
2
- import { type BufferType, ResizeableTypedArray } from "./typed-arrays"
1
+ /**
2
+ * Deduplicated UTF-8 string storage.
3
+ *
4
+ * Strings are stored in a flat buffer and referenced by index.
5
+ * Supports fast deduplication (string→index) and lazy reconstruction (index→string).
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import type { OsmPbfStringTable } from "@osmix/pbf"
11
+ import { type BufferType, ResizeableTypedArray as RTA } from "./typed-arrays"
12
+ import type { ContentHasher } from "./utils"
3
13
 
4
- export interface StringTableTransferables {
5
- bytes: BufferType
6
- start: BufferType
7
- count: BufferType
14
+ /**
15
+ * Serializable state for worker transfer.
16
+ */
17
+ 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
8
24
  }
9
25
 
10
26
  /**
11
- * StringTable that can be used to store and retrieve strings as bytes.
27
+ * Append-only deduplicated string table.
28
+ *
29
+ * Limits: Max string length 65,535 bytes.
30
+ * Rebuilds reverse index lazily after transfer.
12
31
  */
13
32
  export default class StringTable {
33
+ /** UTF-8 encoder for string→bytes conversion */
14
34
  private enc = new TextEncoder()
35
+ /** UTF-8 decoder for bytes→string conversion */
15
36
  private dec = new TextDecoder() // UTF-8
16
37
 
17
- // Serializable state
18
- private bytes: ResizeableTypedArray<Uint8Array>
19
- private start: ResizeableTypedArray<Uint32Array>
20
- private count: ResizeableTypedArray<Uint16Array>
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>
21
45
 
22
- // Builder state
46
+ // ─── Builder State (string → index) ────────────────────────────────────────
47
+ /** Forward lookup: string → index (populated during add) */
23
48
  private stringToIndex = new Map<string, number>()
24
49
 
25
- // Retrieval state
26
- private indexToString = new Map<number, string>()
50
+ /** Whether the reverse index has been built (lazy after transfer) */
51
+ private reverseIndexBuilt = false
27
52
 
28
- static from({ bytes, start, count }: StringTableTransferables): StringTable {
29
- const builder = new StringTable()
30
- builder.bytes = ResizeableTypedArray.from(Uint8Array, bytes)
31
- builder.start = ResizeableTypedArray.from(Uint32Array, start)
32
- builder.count = ResizeableTypedArray.from(Uint16Array, count)
33
- return builder
34
- }
53
+ // ─── Retrieval Cache (index → string) ──────────────────────────────────────
54
+ /** Cache of decoded strings to avoid repeated UTF-8 decoding */
55
+ private indexToString = new Map<number, string>()
35
56
 
36
- constructor() {
37
- this.bytes = new ResizeableTypedArray(Uint8Array)
38
- this.start = new ResizeableTypedArray(Uint32Array)
39
- this.count = new ResizeableTypedArray(Uint16Array)
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)
40
70
  }
41
71
 
72
+ /**
73
+ * Get transferable objects for passing to another thread.
74
+ */
42
75
  transferables(): StringTableTransferables {
43
76
  return {
44
77
  bytes: this.bytes.array.buffer,
@@ -47,6 +80,9 @@ export default class StringTable {
47
80
  }
48
81
  }
49
82
 
83
+ /**
84
+ * Add a string to the table and return its index.
85
+ */
50
86
  add(str: string): number {
51
87
  const existingIndex = this.stringToIndex.get(str)
52
88
  if (existingIndex !== undefined) return existingIndex
@@ -63,19 +99,24 @@ export default class StringTable {
63
99
  * Decode all the strings in a primitive block and add them to the string table.
64
100
  * Return a mapping of block index -> string table index
65
101
  */
66
- createBlockIndexMap(block: OsmPbfBlock) {
67
- const map = new Map<number, number>()
68
- for (let i = 0; i < block.stringtable.length; i++) {
69
- const bytesString = block.stringtable[i]
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]
70
106
  const str = this.dec.decode(bytesString)
71
- const index = this.add(str)
72
- map.set(i, index)
107
+ const existingIndex = this.stringToIndex.get(str)
108
+ if (existingIndex !== undefined) {
109
+ index[i] = existingIndex
110
+ continue
111
+ }
112
+ index[i] = this.add(str)
73
113
  }
74
- return map
114
+ return index
75
115
  }
76
116
 
77
117
  /**
78
- * TextDecoder.decode() does not support SharedArrayBuffer, so we need to first copy to a normal ArrayBuffer before decoding.
118
+ * Get a string by its index.
119
+ * Caches results to avoid repeated UTF-8 decoding.
79
120
  */
80
121
  get(index: number): string {
81
122
  const string = this.indexToString.get(index)
@@ -89,27 +130,39 @@ export default class StringTable {
89
130
  return decoded
90
131
  }
91
132
 
133
+ /**
134
+ * Get the raw UTF-8 bytes of a string.
135
+ * Returns a subarray view (not a copy).
136
+ */
92
137
  getBytes(index: number): Uint8Array {
93
138
  if (index < 0 || index >= this.length)
94
139
  throw Error(`String index out of range: ${index}`)
95
140
  const start = this.start.at(index)
96
141
  const count = this.count.at(index)
97
- // Important: subarray creates a *view*, not a copy; TextDecoder cannot read from it directly.
98
142
  return this.bytes.array.subarray(start, start + count)
99
143
  }
100
144
 
145
+ /** Number of strings in the table. */
101
146
  get length() {
102
147
  return this.start.length
103
148
  }
104
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
+ */
105
156
  buildIndex() {
106
157
  this.bytes.compact()
107
158
  this.start.compact()
108
159
  this.count.compact()
109
-
110
- this.stringToIndex.clear()
160
+ this.reverseIndexBuilt = true
111
161
  }
112
162
 
163
+ /**
164
+ * Convert the string table to an OSM PBF string table.
165
+ */
113
166
  toOsmPbfStringTable(): OsmPbfStringTable {
114
167
  const stringTable: OsmPbfStringTable = []
115
168
  for (let i = 0; i < this.length; i++) {
@@ -117,4 +170,38 @@ export default class StringTable {
117
170
  }
118
171
  return stringTable
119
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
+ }
120
207
  }
package/src/tags.ts CHANGED
@@ -1,54 +1,109 @@
1
- import type { OsmTags } from "@osmix/json"
2
- import type StringTable from "./stringtable"
3
- import { type BufferType, ResizeableTypedArray } from "./typed-arrays"
4
-
5
- export interface TagsTransferables {
6
- tagStart: BufferType
7
- tagCount: BufferType
8
- tagKeys: BufferType
9
- tagVals: BufferType
1
+ /**
2
+ * Tag storage and lookup.
3
+ *
4
+ * Stores key=value pairs using string table indices. Supports:
5
+ * 1. **Entity → Tags**: Retrieve tags for a given entity.
6
+ * 2. **Key → Entities**: Find entities with a specific tag key.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { OsmTags } from "@osmix/shared/types"
12
+ import StringTable from "./stringtable"
13
+ import { type BufferType, ResizeableTypedArray as RTA } from "./typed-arrays"
14
+ import type { ContentHasher } from "./utils"
15
+
16
+ /**
17
+ * Serializable state for worker transfer.
18
+ */
19
+ export interface TagsTransferables<T extends BufferType = BufferType> {
20
+ /** Maps entity index → start position in tagKeys/tagVals. */
21
+ tagStart: T
22
+ /** Maps entity index → number of tags. */
23
+ tagCount: T
24
+ /** Flattened tag key indices. */
25
+ tagKeys: T
26
+ /** Flattened tag value indices. */
27
+ tagVals: T
28
+
29
+ /** Flattened entity indices for reverse key lookup. */
30
+ keyEntities: T
31
+ /** Maps key index → start position in keyEntities. */
32
+ keyIndexStart: T
33
+ /** Maps key index → count of entities with that key. */
34
+ keyIndexCount: T
10
35
  }
11
36
 
37
+ /**
38
+ * Bidirectional tag storage.
39
+ *
40
+ * Limits: Max 255 tags per entity.
41
+ * Note: String indices reference a shared `StringTable`.
42
+ */
12
43
  export class Tags {
13
- private stringTable: StringTable
14
- private tagStart: ResizeableTypedArray<Uint32Array>
15
- private tagCount: ResizeableTypedArray<Uint8Array>
16
- private tagKeys: ResizeableTypedArray<Uint32Array>
17
- private tagVals: ResizeableTypedArray<Uint32Array>
44
+ /** Reference to the shared string table for key/value storage */
45
+ private stringTable: StringTable = new StringTable()
18
46
 
19
- private indexBuilt = false
47
+ // ─── Entity → Tag Lookup ───────────────────────────────────────────────────
48
+ /** Maps entity index → start position in tagKeys/tagVals */
49
+ private tagStart: RTA<Uint32Array>
50
+ /** Maps entity index → number of tags (max 255) */
51
+ private tagCount: RTA<Uint8Array>
52
+ /** All tag key string indices, concatenated */
53
+ private tagKeys: RTA<Uint32Array>
54
+ /** All tag value string indices, concatenated (parallel to tagKeys) */
55
+ private tagVals: RTA<Uint32Array>
20
56
 
21
- static from(
22
- stringTable: StringTable,
23
- { tagStart, tagCount, tagKeys, tagVals }: TagsTransferables,
24
- ) {
25
- const tagIndex = new Tags(stringTable)
26
- tagIndex.tagStart = ResizeableTypedArray.from(Uint32Array, tagStart)
27
- tagIndex.tagCount = ResizeableTypedArray.from(Uint8Array, tagCount)
28
- tagIndex.tagKeys = ResizeableTypedArray.from(Uint32Array, tagKeys)
29
- tagIndex.tagVals = ResizeableTypedArray.from(Uint32Array, tagVals)
30
- tagIndex.indexBuilt = true
31
- return tagIndex
32
- }
57
+ // ─── Key → Entity Reverse Lookup ───────────────────────────────────────────
58
+ /**
59
+ * Flattened array of entity indices that have each tag key.
60
+ * Indexed via keyIndexStart and keyIndexCount.
61
+ */
62
+ private keyEntities: RTA<Uint32Array>
33
63
 
34
- constructor(stringTable: StringTable) {
35
- this.stringTable = stringTable
36
- this.tagStart = new ResizeableTypedArray(Uint32Array)
37
- this.tagCount = new ResizeableTypedArray(Uint8Array)
38
- this.tagKeys = new ResizeableTypedArray(Uint32Array)
39
- this.tagVals = new ResizeableTypedArray(Uint32Array)
40
- }
64
+ /** Maps key string index → start position in keyEntities */
65
+ private keyIndexStart: RTA<Uint32Array>
66
+ /** Maps key string index → count of entities with that key */
67
+ private keyIndexCount: RTA<Uint32Array>
41
68
 
42
- transferables() {
43
- return {
44
- tagStart: this.tagStart.array.buffer,
45
- tagCount: this.tagCount.array.buffer,
46
- tagKeys: this.tagKeys.array.buffer,
47
- tagVals: this.tagVals.array.buffer,
69
+ /**
70
+ * Temporary map used during ingestion to collect entity indices per key.
71
+ * Converted to flat arrays during buildIndex() and then cleared.
72
+ */
73
+ private keyEntityIndexBuilder = new Map<number, number[]>()
74
+
75
+ /** Whether buildIndex() has been called */
76
+ private indexBuilt = false
77
+
78
+ /**
79
+ * Create a new Tags index.
80
+ */
81
+ constructor(stringTable: StringTable, transferables?: TagsTransferables) {
82
+ this.stringTable = stringTable
83
+ if (transferables) {
84
+ this.tagStart = RTA.from(Uint32Array, transferables.tagStart)
85
+ this.tagCount = RTA.from(Uint8Array, transferables.tagCount)
86
+ this.tagKeys = RTA.from(Uint32Array, transferables.tagKeys)
87
+ this.tagVals = RTA.from(Uint32Array, transferables.tagVals)
88
+ this.keyEntities = RTA.from(Uint32Array, transferables.keyEntities)
89
+ this.keyIndexStart = RTA.from(Uint32Array, transferables.keyIndexStart)
90
+ this.keyIndexCount = RTA.from(Uint32Array, transferables.keyIndexCount)
91
+ this.indexBuilt = true
92
+ } else {
93
+ this.tagStart = new RTA(Uint32Array)
94
+ this.tagCount = new RTA(Uint8Array)
95
+ this.tagKeys = new RTA(Uint32Array)
96
+ this.tagVals = new RTA(Uint32Array)
97
+ this.keyEntities = new RTA(Uint32Array)
98
+ this.keyIndexStart = new RTA(Uint32Array)
99
+ this.keyIndexCount = new RTA(Uint32Array)
48
100
  }
49
101
  }
50
102
 
51
- addTags(tags?: OsmTags) {
103
+ /**
104
+ * Add tags to an entity.
105
+ */
106
+ addTags(index: number, tags?: OsmTags): [number[], number[]] {
52
107
  const tagKeys: number[] = []
53
108
  const tagValues: number[] = []
54
109
 
@@ -59,32 +114,73 @@ export class Tags {
59
114
  }
60
115
  }
61
116
 
62
- this.addTagKeysAndValues(tagKeys, tagValues)
117
+ this.addTagKeysAndValues(index, tagKeys, tagValues)
118
+
119
+ return [tagKeys, tagValues]
63
120
  }
64
121
 
65
- addTagKeysAndValues(keys: number[], values: number[]) {
66
- this.tagStart.push(this.tagKeys.length)
67
- this.tagCount.push(keys.length)
122
+ /**
123
+ * Add tags to an entity using key and value indexes.
124
+ */
125
+ addTagKeysAndValues(index: number, keys: number[], values: number[]) {
126
+ this.tagStart.set(index, this.tagKeys.length)
127
+ this.tagCount.set(index, keys.length)
68
128
  this.tagKeys.pushMany(keys)
69
129
  this.tagVals.pushMany(values)
130
+
131
+ keys.forEach((key) => {
132
+ const keyEntities = this.keyEntityIndexBuilder.get(key)
133
+ if (keyEntities) {
134
+ keyEntities.push(index)
135
+ } else {
136
+ this.keyEntityIndexBuilder.set(key, [index])
137
+ }
138
+ })
70
139
  }
71
140
 
141
+ /**
142
+ * Finalize the tag index.
143
+ *
144
+ * Compacts arrays and builds the reverse key→entity index.
145
+ * Must be called before `hasKey()`.
146
+ */
72
147
  buildIndex() {
73
148
  this.tagStart.compact()
74
149
  this.tagCount.compact()
75
150
  this.tagKeys.compact()
76
151
  this.tagVals.compact()
152
+
153
+ // Convert the builder map to flat arrays for the reverse index
154
+ for (const [keyIndex, entityIndexes] of this.keyEntityIndexBuilder) {
155
+ this.keyIndexStart.set(keyIndex, this.keyEntities.length)
156
+ this.keyIndexCount.set(keyIndex, entityIndexes.length)
157
+ this.keyEntities.pushMany(entityIndexes)
158
+ }
159
+
160
+ this.keyIndexStart.compact()
161
+ this.keyIndexCount.compact()
162
+ this.keyEntityIndexBuilder.clear()
163
+
77
164
  this.indexBuilt = true
78
165
  }
79
166
 
80
- get isReady() {
167
+ /**
168
+ * Check if the index is built and ready for use.
169
+ */
170
+ isReady() {
81
171
  return this.indexBuilt
82
172
  }
83
173
 
174
+ /**
175
+ * Get the number of tags for an entity.
176
+ */
84
177
  cardinality(index: number): number {
85
178
  return this.tagCount.at(index) ?? 0
86
179
  }
87
180
 
181
+ /**
182
+ * Get the tags for an entity.
183
+ */
88
184
  getTags(index: number): OsmTags | undefined {
89
185
  const tagCount = this.tagCount.at(index) ?? 0
90
186
  if (tagCount === 0) return
@@ -108,6 +204,9 @@ export class Tags {
108
204
  return tags
109
205
  }
110
206
 
207
+ /**
208
+ * Get tags from key and value indexes.
209
+ */
111
210
  getTagsFromIndices(keys: number[], values: number[]): OsmTags {
112
211
  const tags: OsmTags = {}
113
212
  for (let i = 0; i < keys.length; i++) {
@@ -119,4 +218,79 @@ export class Tags {
119
218
  }
120
219
  return tags
121
220
  }
221
+
222
+ /**
223
+ * Find the index of a tag key.
224
+ */
225
+ find(key: string): number {
226
+ return this.stringTable.find(key)
227
+ }
228
+
229
+ /**
230
+ * Get all entity indexes that have a specific tag key.
231
+ */
232
+ hasKey(keyIndex: number): number[] {
233
+ if (keyIndex < 0) return []
234
+ const start = this.keyIndexStart.at(keyIndex) ?? 0
235
+ const count = this.keyIndexCount.at(keyIndex) ?? 0
236
+ return Array.from(this.keyEntities.array.subarray(start, start + count))
237
+ }
238
+
239
+ /**
240
+ * Create a unique composite index for a key=value pair.
241
+ * Uses row-major indexing: `key * width + val`.
242
+ */
243
+ kvToIndex(key: number, val: number) {
244
+ const width = this.stringTable.length
245
+ return key * width + val
246
+ }
247
+
248
+ /**
249
+ * Get transferable objects for passing to another thread.
250
+ */
251
+ transferables(): TagsTransferables {
252
+ return {
253
+ tagStart: this.tagStart.array.buffer,
254
+ tagCount: this.tagCount.array.buffer,
255
+ tagKeys: this.tagKeys.array.buffer,
256
+ tagVals: this.tagVals.array.buffer,
257
+ keyEntities: this.keyEntities.array.buffer,
258
+ keyIndexStart: this.keyIndexStart.array.buffer,
259
+ keyIndexCount: this.keyIndexCount.array.buffer,
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Get the approximate memory requirements for a given number of tags in bytes.
265
+ */
266
+ static getBytesRequired(count: number) {
267
+ return (
268
+ count * Uint32Array.BYTES_PER_ELEMENT + // tagStart
269
+ count * Uint8Array.BYTES_PER_ELEMENT // tagCount
270
+ )
271
+ }
272
+
273
+ /**
274
+ * Reconstruct a Tags index from transferable objects.
275
+ */
276
+ static fromTransferables(
277
+ stringTable: StringTable,
278
+ transferables: TagsTransferables,
279
+ ) {
280
+ const tagIndex = new Tags(stringTable, transferables)
281
+ tagIndex.indexBuilt = true
282
+ return tagIndex
283
+ }
284
+
285
+ /**
286
+ * Update a ContentHasher with the tags data.
287
+ * Hashes tag keys and values for each entity.
288
+ */
289
+ updateHash(hasher: ContentHasher): ContentHasher {
290
+ return hasher
291
+ .update(this.tagStart.array)
292
+ .update(this.tagCount.array)
293
+ .update(this.tagKeys.array)
294
+ .update(this.tagVals.array)
295
+ }
122
296
  }