@honua/react 0.1.12 → 0.1.13

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 (190) hide show
  1. package/columnar/apache-arrow.d.ts +17 -0
  2. package/columnar/apache-arrow.d.ts.map +1 -0
  3. package/columnar/apache-arrow.js +1036 -0
  4. package/columnar/batch-cache-conformance.d.ts +66 -0
  5. package/columnar/batch-cache-conformance.d.ts.map +1 -0
  6. package/columnar/batch-cache-conformance.js +317 -0
  7. package/columnar/batch-cache-indexeddb.d.ts +31 -0
  8. package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
  9. package/columnar/batch-cache-indexeddb.js +148 -0
  10. package/columnar/batch-cache.d.ts +298 -0
  11. package/columnar/batch-cache.d.ts.map +1 -0
  12. package/columnar/batch-cache.js +757 -0
  13. package/columnar/geoarrow-aggregate.d.ts +178 -0
  14. package/columnar/geoarrow-aggregate.d.ts.map +1 -0
  15. package/columnar/geoarrow-aggregate.js +976 -0
  16. package/columnar/geoarrow-patch.d.ts +382 -0
  17. package/columnar/geoarrow-patch.d.ts.map +1 -0
  18. package/columnar/geoarrow-patch.js +1656 -0
  19. package/columnar/geoarrow-reproject.d.ts +22 -0
  20. package/columnar/geoarrow-reproject.d.ts.map +1 -0
  21. package/columnar/geoarrow-reproject.js +119 -0
  22. package/columnar/geoarrow-serialization.d.ts +68 -0
  23. package/columnar/geoarrow-serialization.d.ts.map +1 -0
  24. package/columnar/geoarrow-serialization.js +331 -0
  25. package/columnar/geoarrow-types.d.ts +271 -0
  26. package/columnar/geoarrow-types.d.ts.map +1 -0
  27. package/columnar/geoarrow-types.js +15 -0
  28. package/columnar/geoarrow.d.ts +52 -0
  29. package/columnar/geoarrow.d.ts.map +1 -0
  30. package/columnar/geoarrow.js +1469 -0
  31. package/columnar/index.d.ts +43 -0
  32. package/columnar/index.d.ts.map +1 -0
  33. package/columnar/index.js +29 -0
  34. package/columnar/operators.d.ts +70 -0
  35. package/columnar/operators.d.ts.map +1 -0
  36. package/columnar/operators.js +307 -0
  37. package/columnar/result.d.ts +369 -0
  38. package/columnar/result.d.ts.map +1 -0
  39. package/columnar/result.js +815 -0
  40. package/columnar/telemetry.d.ts +152 -0
  41. package/columnar/telemetry.d.ts.map +1 -0
  42. package/columnar/telemetry.js +265 -0
  43. package/columnar/transfer.d.ts +28 -0
  44. package/columnar/transfer.d.ts.map +1 -0
  45. package/columnar/transfer.js +669 -0
  46. package/columnar/types.d.ts +175 -0
  47. package/columnar/types.d.ts.map +1 -0
  48. package/columnar/types.js +22 -0
  49. package/columnar/worker.d.ts +157 -0
  50. package/columnar/worker.d.ts.map +1 -0
  51. package/columnar/worker.js +1059 -0
  52. package/connect-url-safety.d.ts +53 -0
  53. package/connect-url-safety.js +189 -0
  54. package/contract/pmtiles.d.ts +6 -5
  55. package/contract/pmtiles.d.ts.map +1 -1
  56. package/contract/pmtiles.js +30 -5
  57. package/core/client.d.ts.map +1 -1
  58. package/core/client.js +87 -19
  59. package/core/deprecation-notice.d.ts +28 -0
  60. package/core/deprecation-notice.d.ts.map +1 -0
  61. package/core/deprecation-notice.js +22 -0
  62. package/core/esri-geojson.d.ts.map +1 -1
  63. package/core/esri-geojson.js +10 -6
  64. package/core/optional-grpc-peer.d.ts +15 -0
  65. package/core/optional-grpc-peer.d.ts.map +1 -0
  66. package/core/optional-grpc-peer.js +60 -0
  67. package/esri-compat/basemap-gallery.d.ts +2 -0
  68. package/esri-compat/basemap-gallery.d.ts.map +1 -1
  69. package/esri-compat/basemap-gallery.js +12 -0
  70. package/esri-compat/basemap.d.ts +4 -0
  71. package/esri-compat/basemap.d.ts.map +1 -1
  72. package/esri-compat/basemap.js +70 -0
  73. package/esri-compat/bookmarks.d.ts +2 -0
  74. package/esri-compat/bookmarks.d.ts.map +1 -1
  75. package/esri-compat/bookmarks.js +12 -0
  76. package/esri-compat/controls.d.ts +18 -0
  77. package/esri-compat/controls.d.ts.map +1 -1
  78. package/esri-compat/controls.js +120 -57
  79. package/esri-compat/directions.d.ts +4 -0
  80. package/esri-compat/directions.d.ts.map +1 -1
  81. package/esri-compat/directions.js +30 -20
  82. package/esri-compat/editor.d.ts +4 -0
  83. package/esri-compat/editor.d.ts.map +1 -1
  84. package/esri-compat/editor.js +16 -0
  85. package/esri-compat/esri-config.d.ts +2 -1
  86. package/esri-compat/esri-config.d.ts.map +1 -1
  87. package/esri-compat/feature-effect.d.ts +23 -0
  88. package/esri-compat/feature-effect.d.ts.map +1 -0
  89. package/esri-compat/feature-effect.js +12 -0
  90. package/esri-compat/feature-form.d.ts +6 -0
  91. package/esri-compat/feature-form.d.ts.map +1 -1
  92. package/esri-compat/feature-form.js +63 -0
  93. package/esri-compat/feature-layer.d.ts +21 -2
  94. package/esri-compat/feature-layer.d.ts.map +1 -1
  95. package/esri-compat/feature-layer.js +172 -1
  96. package/esri-compat/feature-table.d.ts +5 -0
  97. package/esri-compat/feature-table.d.ts.map +1 -1
  98. package/esri-compat/feature-table.js +23 -0
  99. package/esri-compat/identity-manager.d.ts +1 -1
  100. package/esri-compat/identity-manager.js +1 -1
  101. package/esri-compat/legend.d.ts +2 -0
  102. package/esri-compat/legend.d.ts.map +1 -1
  103. package/esri-compat/legend.js +98 -0
  104. package/esri-compat/locator.d.ts +1 -1
  105. package/esri-compat/locator.js +1 -1
  106. package/esri-compat/map-view-mount.d.ts +1 -0
  107. package/esri-compat/map-view-mount.d.ts.map +1 -1
  108. package/esri-compat/map-view-mount.js +46 -25
  109. package/esri-compat/map-view.d.ts +13 -0
  110. package/esri-compat/map-view.d.ts.map +1 -1
  111. package/esri-compat/map-view.js +94 -8
  112. package/esri-compat/map.d.ts +5 -1
  113. package/esri-compat/map.d.ts.map +1 -1
  114. package/esri-compat/map.js +10 -0
  115. package/esri-compat/measurement.d.ts +2 -0
  116. package/esri-compat/measurement.d.ts.map +1 -1
  117. package/esri-compat/measurement.js +14 -0
  118. package/esri-compat/point.d.ts +7 -0
  119. package/esri-compat/point.d.ts.map +1 -1
  120. package/esri-compat/point.js +33 -3
  121. package/esri-compat/popup.d.ts +2 -0
  122. package/esri-compat/popup.d.ts.map +1 -1
  123. package/esri-compat/popup.js +12 -0
  124. package/esri-compat/portal.d.ts +1 -1
  125. package/esri-compat/portal.js +1 -1
  126. package/esri-compat/print.d.ts +3 -0
  127. package/esri-compat/print.d.ts.map +1 -1
  128. package/esri-compat/print.js +15 -0
  129. package/esri-compat/search.d.ts +4 -0
  130. package/esri-compat/search.d.ts.map +1 -1
  131. package/esri-compat/search.js +25 -28
  132. package/esri-compat/sketch.d.ts +2 -0
  133. package/esri-compat/sketch.d.ts.map +1 -1
  134. package/esri-compat/sketch.js +12 -0
  135. package/esri-compat/widget-host.d.ts +46 -17
  136. package/esri-compat/widget-host.d.ts.map +1 -1
  137. package/esri-compat/widget-host.js +84 -20
  138. package/esri-compat/widget-shell-hosts.d.ts +61 -0
  139. package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
  140. package/esri-compat/widget-shell-hosts.js +76 -0
  141. package/geocoding/index.d.ts +114 -0
  142. package/geocoding/index.d.ts.map +1 -0
  143. package/geocoding/index.js +293 -0
  144. package/geocoding/provider-http.d.ts +37 -0
  145. package/geocoding/provider-http.d.ts.map +1 -0
  146. package/geocoding/provider-http.js +93 -0
  147. package/geocoding/provider.d.ts +145 -0
  148. package/geocoding/provider.d.ts.map +1 -0
  149. package/geocoding/provider.js +41 -0
  150. package/geocoding/providers/honua.d.ts +28 -0
  151. package/geocoding/providers/honua.d.ts.map +1 -0
  152. package/geocoding/providers/honua.js +68 -0
  153. package/geocoding/providers/nominatim.d.ts +47 -0
  154. package/geocoding/providers/nominatim.d.ts.map +1 -0
  155. package/geocoding/providers/nominatim.js +105 -0
  156. package/geocoding/providers/pelias.d.ts +40 -0
  157. package/geocoding/providers/pelias.d.ts.map +1 -0
  158. package/geocoding/providers/pelias.js +115 -0
  159. package/geocoding/providers/photon.d.ts +39 -0
  160. package/geocoding/providers/photon.d.ts.map +1 -0
  161. package/geocoding/providers/photon.js +107 -0
  162. package/geometry/convert.d.ts +13 -0
  163. package/geometry/convert.d.ts.map +1 -0
  164. package/geometry/convert.js +12 -0
  165. package/geometry/index.d.ts +17 -0
  166. package/geometry/index.d.ts.map +1 -0
  167. package/geometry/index.js +15 -0
  168. package/geometry/internal.d.ts +25 -0
  169. package/geometry/internal.d.ts.map +1 -0
  170. package/geometry/internal.js +40 -0
  171. package/geometry/ops.d.ts +71 -0
  172. package/geometry/ops.d.ts.map +1 -0
  173. package/geometry/ops.js +147 -0
  174. package/geometry/project.d.ts +40 -0
  175. package/geometry/project.d.ts.map +1 -0
  176. package/geometry/project.js +88 -0
  177. package/geometry/types.d.ts +36 -0
  178. package/geometry/types.d.ts.map +1 -0
  179. package/geometry/types.js +12 -0
  180. package/kernel/renderer.d.ts +82 -0
  181. package/kernel/renderer.js +1 -0
  182. package/offline/digest.d.ts +15 -0
  183. package/offline/digest.js +58 -0
  184. package/offline/quota.d.ts +161 -0
  185. package/offline/quota.js +370 -0
  186. package/offline/types.d.ts +349 -0
  187. package/offline/types.js +75 -0
  188. package/package.json +14 -2
  189. package/widget-capabilities.d.ts +67 -0
  190. package/widget-capabilities.js +152 -0
@@ -0,0 +1,815 @@
1
+ /**
2
+ * Bounded conversion between Honua columnar batches and the protocol-neutral
3
+ * object `Result` contract.
4
+ *
5
+ * The columnar plane is deliberately one-way at the buffer level: rows go in,
6
+ * GPU-ready binaries come out, and nothing per-feature is ever materialized.
7
+ * That is what keeps a million-row batch affordable, but it also means the
8
+ * first time an application needs a popup, a table page, an export, or an
9
+ * assertion in the shape the rest of the SDK speaks, it has no way back. These
10
+ * two functions are that way back, and they are bounded by construction.
11
+ *
12
+ * **The ceiling is the feature, not a guard rail.** Materializing feature
13
+ * objects costs roughly two orders of magnitude more memory per row than the
14
+ * packed columns they came from, so an unbounded conversion is exactly the
15
+ * silent materialization the columnar plane exists to avoid. `maxFeatures` is
16
+ * therefore a required option on both directions, it is enforced before a
17
+ * single feature object is allocated, and there is no sentinel, no `Infinity`,
18
+ * and no options object that disables it. A conversion that would exceed it
19
+ * fails closed with `HonuaGeoArrowError` code `row-limit-exceeded` naming both
20
+ * the ceiling and the requested count.
21
+ *
22
+ * Conversion reads only the requested window. Payload validation that the
23
+ * unbounded {@link inspectGeoArrowBatch} performs over the whole batch is
24
+ * performed here per materialized row instead, so cost stays proportional to
25
+ * the window rather than to the batch.
26
+ *
27
+ * **Loss is reported, never taken silently.** The forward direction is complete
28
+ * by construction: a normative GeoArrow batch carries exactly a geometry column
29
+ * plus optional temporal, dictionary, and feature-id columns, and every one of
30
+ * them lands on the converted feature. The inverse direction is where an object
31
+ * `Result` can carry more than a columnar batch can hold, so it fails closed on
32
+ * an attribute no column binding covers rather than dropping it, and the only
33
+ * way to proceed is an explicit opt-in that names every dropped attribute back
34
+ * to the caller.
35
+ *
36
+ * **Traversing more than one window is streaming, not a bigger window.**
37
+ * {@link columnarBatchToResultPages} walks a batch as a sequence of bounded
38
+ * pages, holding one page at a time, checking a caller's `AbortSignal` on a row
39
+ * cadence, and yielding to the host task queue between pages so a cancellation
40
+ * can actually land. The ceiling still applies — to each page.
41
+ *
42
+ * @experimental
43
+ */
44
+ import { HonuaGeoArrowError } from "./geoarrow-types.js";
45
+ import { createGeoArrowBatch, decodeGeoArrowGeometryAt, inspectGeoArrowBatchScoped, isGeoArrowRowValid, } from "./geoarrow.js";
46
+ import { beginColumnarSpan } from "./telemetry.js";
47
+ /**
48
+ * Conservative ceiling recommended for a bounded conversion window.
49
+ *
50
+ * It exists because feature objects, unlike the packed columns they are read
51
+ * from, cost hundreds of bytes per row: a hundred thousand point features is
52
+ * already tens of megabytes of live JavaScript objects, which is the largest
53
+ * bounded window that still behaves like an interaction rather than a bulk
54
+ * export. It is exported as a documented starting point, **not** applied
55
+ * implicitly — `maxFeatures` is always explicit at the call site so the cost of
56
+ * materialization is always visible in the calling code.
57
+ */
58
+ export const DEFAULT_COLUMNAR_RESULT_MAX_FEATURES = 100_000;
59
+ /**
60
+ * Rows converted between two `AbortSignal` checks.
61
+ *
62
+ * Small enough that a cancelled page stops promptly even at the ceiling, large
63
+ * enough that the check is not measurable against the per-row cost of building
64
+ * a feature object. It is a constant rather than an option because a caller
65
+ * tuning it could only make cancellation worse.
66
+ */
67
+ const CANCELLATION_CHECK_INTERVAL_ROWS = 1_024;
68
+ /**
69
+ * Rows converted between two task-queue yields inside a single page.
70
+ *
71
+ * Checking a signal is worthless if nothing can ever set it: JavaScript is
72
+ * single-threaded, so an uninterrupted synchronous loop cannot observe an abort
73
+ * no matter how often it polls. A page therefore hands control back to the host
74
+ * on this cadence, which is what gives the poll above something to find. Pages
75
+ * smaller than this — the common case — pay nothing.
76
+ */
77
+ const YIELD_INTERVAL_ROWS = 16_384;
78
+ const GEOJSON_TYPE_BY_KIND = Object.freeze({
79
+ point: "Point",
80
+ linestring: "LineString",
81
+ polygon: "Polygon",
82
+ });
83
+ const KIND_BY_GEOJSON_TYPE = Object.freeze({
84
+ Point: "point",
85
+ LineString: "linestring",
86
+ Polygon: "polygon",
87
+ });
88
+ function fail(code, message, detail, cause) {
89
+ throw new HonuaGeoArrowError(code, message, detail, cause === undefined ? undefined : { cause });
90
+ }
91
+ /**
92
+ * Validate the ceiling itself. Separate from the range because the paged
93
+ * traversal applies the ceiling to each page rather than to the whole walk.
94
+ */
95
+ function requireCeiling(maxFeatures, label) {
96
+ if (!Number.isSafeInteger(maxFeatures) || maxFeatures <= 0) {
97
+ fail("invalid-input", `${label} requires a positive safe-integer maxFeatures ceiling; there is no unbounded conversion mode.`, { resource: "maxFeatures", actual: maxFeatures });
98
+ }
99
+ return maxFeatures;
100
+ }
101
+ /**
102
+ * Resolve the contiguous row range a caller asked for, without applying any
103
+ * ceiling. Rejections here are against plain counts, so nothing is read.
104
+ */
105
+ function resolveRange(available, options, label) {
106
+ const offset = options.offset ?? 0;
107
+ if (!Number.isSafeInteger(offset) || offset < 0) {
108
+ fail("invalid-input", `${label} offset must be a non-negative safe integer.`, {
109
+ resource: "offset",
110
+ actual: offset,
111
+ });
112
+ }
113
+ if (offset > available) {
114
+ fail("invalid-input", `${label} offset ${offset} is past the end of the ${available}-row window source.`, {
115
+ resource: "offset",
116
+ actual: offset,
117
+ available,
118
+ });
119
+ }
120
+ const remaining = available - offset;
121
+ const limit = options.limit ?? remaining;
122
+ if (!Number.isSafeInteger(limit) || limit < 0) {
123
+ fail("invalid-input", `${label} limit must be a non-negative safe integer.`, {
124
+ resource: "limit",
125
+ actual: limit,
126
+ });
127
+ }
128
+ return { offset, count: Math.min(limit, remaining) };
129
+ }
130
+ /**
131
+ * Resolve and bound the window before anything is read or allocated.
132
+ *
133
+ * Every rejection here happens against plain counts, so an over-ceiling request
134
+ * costs one comparison and produces no feature object, no buffer view, and no
135
+ * batch inspection.
136
+ */
137
+ function resolveWindow(available, options, label) {
138
+ const maxFeatures = requireCeiling(options.maxFeatures, label);
139
+ const range = resolveRange(available, options, label);
140
+ if (range.count > maxFeatures) {
141
+ fail("row-limit-exceeded", `${label} requested ${range.count} features; the maxFeatures ceiling is ${maxFeatures}.`, { resource: "maxFeatures", requested: range.count, limit: maxFeatures });
142
+ }
143
+ return range;
144
+ }
145
+ /**
146
+ * Throw the platform's own cancellation error when the caller's signal is
147
+ * already aborted. `DOMException`/`AbortError` rather than
148
+ * {@link HonuaGeoArrowError}, because a cancelled conversion is not a
149
+ * conversion failure and callers already branch on `error.name === "AbortError"`
150
+ * for every other cancellable path in the SDK.
151
+ */
152
+ function throwIfAborted(signal) {
153
+ if (signal?.aborted !== true)
154
+ return;
155
+ throw new DOMException("The operation was aborted", "AbortError");
156
+ }
157
+ /**
158
+ * Yield to the host task queue so a queued abort is actually delivered.
159
+ *
160
+ * A microtask checkpoint is not enough: the events that abort a traversal —
161
+ * a worker `postMessage`, a timer, a user gesture — are all delivered as tasks,
162
+ * so an `await` that only drains microtasks would starve every one of them and
163
+ * make the signal check below theatre.
164
+ */
165
+ function yieldToHost() {
166
+ return new Promise((resolve) => {
167
+ const immediate = globalThis.setImmediate;
168
+ if (typeof immediate === "function") {
169
+ immediate(resolve);
170
+ return;
171
+ }
172
+ if (typeof MessageChannel === "function") {
173
+ const channel = new MessageChannel();
174
+ channel.port1.onmessage = () => {
175
+ channel.port1.close();
176
+ channel.port2.close();
177
+ resolve();
178
+ };
179
+ channel.port1.start();
180
+ channel.port2.postMessage(undefined);
181
+ return;
182
+ }
183
+ setTimeout(resolve, 0);
184
+ });
185
+ }
186
+ /**
187
+ * Component count a GeoJSON position carries for a declared GeoArrow layout.
188
+ *
189
+ * `xym` and `xyzm` fail closed: GeoJSON cannot carry an M coordinate, and
190
+ * dropping one silently would report a lossy conversion as a faithful one.
191
+ */
192
+ function geoJsonComponents(dimensions) {
193
+ if (dimensions === "xy")
194
+ return 2;
195
+ if (dimensions === "xyz")
196
+ return 3;
197
+ fail("unsupported-layout", `GeoJSON has no representation for an M coordinate, so "${dimensions}" geometry cannot be converted to an object Result.`, { dimensions });
198
+ }
199
+ function assertFinitePosition(position, components, row) {
200
+ if (position.length !== components) {
201
+ fail("invalid-batch", `Geometry at row ${row} carries ${position.length} coordinates; the layout declares ${components}.`, {
202
+ row,
203
+ expected: components,
204
+ actual: position.length,
205
+ });
206
+ }
207
+ for (let index = 0; index < components; index += 1) {
208
+ if (!Number.isFinite(position[index])) {
209
+ fail("invalid-batch", "Coordinate buffers must contain finite values.", { row, component: index });
210
+ }
211
+ }
212
+ return position;
213
+ }
214
+ /**
215
+ * Wrap a decoded GeoArrow geometry as GeoJSON.
216
+ *
217
+ * The decoded position arrays are reused rather than copied: they are already
218
+ * freshly allocated for this window, so re-copying them would double the only
219
+ * per-feature allocation the conversion makes.
220
+ */
221
+ function toGeoJsonGeometry(kind, components, decoded, row) {
222
+ if (kind === "point") {
223
+ const point = decoded;
224
+ return Object.freeze({
225
+ type: GEOJSON_TYPE_BY_KIND.point,
226
+ coordinates: assertFinitePosition(point, components, row),
227
+ });
228
+ }
229
+ if (kind === "linestring") {
230
+ const line = decoded;
231
+ for (const position of line)
232
+ assertFinitePosition(position, components, row);
233
+ return Object.freeze({ type: GEOJSON_TYPE_BY_KIND.linestring, coordinates: line });
234
+ }
235
+ const polygon = decoded;
236
+ for (const ring of polygon)
237
+ for (const position of ring)
238
+ assertFinitePosition(position, components, row);
239
+ return Object.freeze({ type: GEOJSON_TYPE_BY_KIND.polygon, coordinates: polygon });
240
+ }
241
+ /**
242
+ * Decode dictionary values lazily, for the window's distinct indices only.
243
+ *
244
+ * A batch may carry tens of thousands of dictionary values behind a window of
245
+ * a few rows; decoding all of them would make a bounded conversion pay for the
246
+ * whole column.
247
+ */
248
+ function createDictionaryReader(dictionary) {
249
+ const decoder = new TextDecoder("utf-8", { fatal: true });
250
+ const cache = new Map();
251
+ const size = dictionary.offsets.length - 1;
252
+ return (index, row) => {
253
+ const cached = cache.get(index);
254
+ if (cached !== undefined)
255
+ return cached;
256
+ if (!Number.isInteger(index) || index < 0 || index >= size) {
257
+ fail("invalid-batch", `Dictionary index at row ${row} is outside the dictionary.`, { row, index, size });
258
+ }
259
+ let value;
260
+ try {
261
+ value = decoder.decode(dictionary.values.subarray(dictionary.offsets[index], dictionary.offsets[index + 1]));
262
+ }
263
+ catch (cause) {
264
+ fail("invalid-batch", "Dictionary value bytes are not valid UTF-8.", { row, index }, cause);
265
+ }
266
+ cache.set(index, value);
267
+ return value;
268
+ };
269
+ }
270
+ function schemaFieldNullable(batch, name) {
271
+ return batch.schema.fields.find((field) => field.name === name)?.nullable ?? false;
272
+ }
273
+ /**
274
+ * Declared field schema for the attributes the conversion emits, so a consumer
275
+ * reads the attribute types off the `Result` instead of guessing them.
276
+ */
277
+ function describeFields(inspection) {
278
+ const fields = [];
279
+ if (inspection.featureIds) {
280
+ fields.push({ name: inspection.featureIds.field, type: "esriFieldTypeOID", nullable: false, editable: false });
281
+ }
282
+ if (inspection.temporal) {
283
+ fields.push({
284
+ name: inspection.temporal.field,
285
+ type: "esriFieldTypeDate",
286
+ nullable: schemaFieldNullable(inspection.batch, inspection.temporal.field),
287
+ });
288
+ }
289
+ if (inspection.dictionary) {
290
+ fields.push({
291
+ name: inspection.dictionary.field,
292
+ type: "esriFieldTypeString",
293
+ nullable: schemaFieldNullable(inspection.batch, inspection.dictionary.field),
294
+ });
295
+ }
296
+ return fields.length === 0 ? undefined : Object.freeze(fields);
297
+ }
298
+ function attributeBinding(inspection) {
299
+ return Object.freeze({
300
+ ...(inspection.featureIds === undefined
301
+ ? {}
302
+ : { featureId: Object.freeze({ attribute: inspection.featureIds.field }) }),
303
+ ...(inspection.temporal === undefined
304
+ ? {}
305
+ : {
306
+ temporal: Object.freeze({
307
+ attribute: inspection.temporal.field,
308
+ unit: inspection.temporal.unit,
309
+ ...(inspection.temporal.timezone === undefined ? {} : { timezone: inspection.temporal.timezone }),
310
+ }),
311
+ }),
312
+ ...(inspection.dictionary === undefined
313
+ ? {}
314
+ : {
315
+ dictionary: Object.freeze({
316
+ attribute: inspection.dictionary.field,
317
+ ordered: inspection.dictionary.ordered,
318
+ }),
319
+ }),
320
+ });
321
+ }
322
+ function prepareConversion(batch, limits) {
323
+ const inspection = inspectGeoArrowBatchScoped(batch, limits, "window");
324
+ const fields = describeFields(inspection);
325
+ return {
326
+ inspection,
327
+ components: geoJsonComponents(inspection.geometry.dimensions),
328
+ ...(inspection.dictionary === undefined ? {} : { readDictionary: createDictionaryReader(inspection.dictionary) }),
329
+ ...(fields === undefined ? {} : { fields }),
330
+ attributes: attributeBinding(inspection),
331
+ };
332
+ }
333
+ /**
334
+ * Materialize one contiguous row range as typed features, in ascending row
335
+ * order, appending to `features`.
336
+ */
337
+ function convertRows(prepared, offset, count, features) {
338
+ const { inspection, components, readDictionary } = prepared;
339
+ const geometry = inspection.geometry;
340
+ const end = offset + count;
341
+ for (let row = offset; row < end; row += 1) {
342
+ const attributes = {};
343
+ if (inspection.featureIds !== undefined) {
344
+ attributes[inspection.featureIds.field] = inspection.featureIds.values[row];
345
+ }
346
+ if (inspection.temporal !== undefined) {
347
+ attributes[inspection.temporal.field] = isGeoArrowRowValid(inspection.temporal.validity, row)
348
+ ? inspection.temporal.values[row]
349
+ : null;
350
+ }
351
+ if (inspection.dictionary !== undefined) {
352
+ attributes[inspection.dictionary.field] = isGeoArrowRowValid(inspection.dictionary.validity, row)
353
+ ? readDictionary(inspection.dictionary.indices[row], row)
354
+ : null;
355
+ }
356
+ const decoded = decodeGeoArrowGeometryAt(geometry, row);
357
+ features.push(Object.freeze({
358
+ attributes: Object.freeze(attributes),
359
+ geometry: decoded === null ? null : toGeoJsonGeometry(geometry.kind, components, decoded, row),
360
+ }));
361
+ }
362
+ return features;
363
+ }
364
+ /**
365
+ * Materialize one page cooperatively: poll the signal every
366
+ * {@link CANCELLATION_CHECK_INTERVAL_ROWS} rows, and hand control back to the
367
+ * host every {@link YIELD_INTERVAL_ROWS} rows so an abort raised by a task can
368
+ * actually reach the poll. Row conversion itself is the same code the
369
+ * synchronous entry point uses, called a chunk at a time.
370
+ */
371
+ async function convertRowsCooperatively(prepared, offset, count, signal) {
372
+ const features = [];
373
+ let sinceYield = 0;
374
+ for (let converted = 0; converted < count; converted += CANCELLATION_CHECK_INTERVAL_ROWS) {
375
+ throwIfAborted(signal);
376
+ const chunk = Math.min(CANCELLATION_CHECK_INTERVAL_ROWS, count - converted);
377
+ convertRows(prepared, offset + converted, chunk, features);
378
+ sinceYield += chunk;
379
+ if (converted + chunk < count && sinceYield >= YIELD_INTERVAL_ROWS) {
380
+ sinceYield = 0;
381
+ await yieldToHost();
382
+ }
383
+ }
384
+ return features;
385
+ }
386
+ /** Assemble one bounded window's `Result` around already-materialized features. */
387
+ function buildResult(prepared, features, window, maxFeatures) {
388
+ const { inspection, fields, attributes } = prepared;
389
+ const geometry = inspection.geometry;
390
+ const batchRowCount = inspection.batch.rowCount;
391
+ const provenance = Object.freeze({
392
+ batchId: inspection.batch.id,
393
+ schemaId: inspection.batch.schema.id,
394
+ sequence: inspection.batch.sequence,
395
+ batchRowCount,
396
+ offset: window.offset,
397
+ count: window.count,
398
+ rowOffset: (inspection.batch.rowOffset ?? 0) + window.offset,
399
+ maxFeatures,
400
+ geometry: Object.freeze({
401
+ kind: geometry.kind,
402
+ field: geometry.field,
403
+ dimensions: geometry.dimensions,
404
+ coordinateLayout: geometry.coordinateLayout,
405
+ edges: geometry.edges,
406
+ }),
407
+ attributes,
408
+ identity: inspection.batch.identity,
409
+ });
410
+ return Object.freeze({
411
+ features: Object.freeze(features),
412
+ exceededTransferLimit: window.count < batchRowCount,
413
+ totalCount: batchRowCount,
414
+ ...(fields === undefined ? {} : { fields }),
415
+ ...(geometry.crs === undefined ? {} : { crs: geometry.crs }),
416
+ columnar: provenance,
417
+ });
418
+ }
419
+ function assertConvertibleBatch(batch, label) {
420
+ if (typeof batch !== "object" || batch === null)
421
+ fail("invalid-input", "batch must be a columnar batch object.");
422
+ const batchRowCount = batch.rowCount;
423
+ if (!Number.isSafeInteger(batchRowCount) || batchRowCount < 0) {
424
+ fail("invalid-batch", `${label} requires batch.rowCount to be a non-negative safe integer.`);
425
+ }
426
+ return batchRowCount;
427
+ }
428
+ /**
429
+ * Convert a bounded, contiguous row window of a columnar batch into a
430
+ * protocol-neutral {@link Result} of typed features.
431
+ *
432
+ * Features are emitted in batch row order, so the `Result` preserves the
433
+ * batch's declared ordering exactly. Geometry becomes GeoJSON, the batch's
434
+ * feature-id, timestamp, and dictionary columns become attributes under their
435
+ * declared names, and a null timestamp or dictionary value becomes an explicit
436
+ * `null` attribute rather than a missing key.
437
+ *
438
+ * This direction is complete: a normative GeoArrow batch carries exactly those
439
+ * four columns, so nothing on the batch is left behind and there is no loss to
440
+ * report. Use {@link columnarBatchToResultPages} to walk more of a batch than
441
+ * one window without raising the ceiling.
442
+ *
443
+ * @throws HonuaGeoArrowError `row-limit-exceeded` when the window exceeds
444
+ * `options.maxFeatures`. The ceiling is checked before the batch is inspected,
445
+ * so nothing is read or allocated on that path.
446
+ * @throws HonuaGeoArrowError `unsupported-layout` for `xym`/`xyzm` geometry,
447
+ * which GeoJSON cannot represent.
448
+ *
449
+ * @example
450
+ * ```ts
451
+ * const result = columnarBatchToResult(batch, { offset: 0, limit: 50, maxFeatures: 200 });
452
+ * for (const feature of result.features) console.log(feature.geometry, feature.attributes);
453
+ * ```
454
+ */
455
+ export function columnarBatchToResult(batch, options) {
456
+ const batchRowCount = assertConvertibleBatch(batch, "columnarBatchToResult");
457
+ if (typeof options !== "object" || options === null) {
458
+ fail("invalid-input", "columnarBatchToResult requires an options object carrying the maxFeatures ceiling.");
459
+ }
460
+ const telemetry = options.telemetry;
461
+ if (telemetry === undefined)
462
+ return convertWindow(batch, options, batchRowCount);
463
+ const span = beginColumnarSpan(telemetry, "columnar-result-conversion", batch.identity, {
464
+ batchId: batch.id,
465
+ maxFeatures: options.maxFeatures,
466
+ });
467
+ try {
468
+ const result = convertWindow(batch, options, batchRowCount);
469
+ span?.finish(conversionSpanDetail(result));
470
+ return result;
471
+ }
472
+ catch (error) {
473
+ span?.fail(error, error instanceof HonuaGeoArrowError ? { code: error.code } : undefined);
474
+ throw error;
475
+ }
476
+ }
477
+ function convertWindow(batch, options, batchRowCount) {
478
+ // Bound first. Nothing below this line runs for an over-ceiling request.
479
+ const window = resolveWindow(batchRowCount, options, "columnarBatchToResult");
480
+ const prepared = prepareConversion(batch, options.limits ?? {});
481
+ return buildResult(prepared, convertRows(prepared, window.offset, window.count, []), window, options.maxFeatures);
482
+ }
483
+ /**
484
+ * The conversion span reports the window the conversion actually produced —
485
+ * the converted feature count and the offsets the `Result` already carries on
486
+ * its provenance, not a new measurement.
487
+ */
488
+ function conversionSpanDetail(result) {
489
+ const provenance = result.columnar;
490
+ return {
491
+ count: provenance.count,
492
+ offset: provenance.offset,
493
+ rowOffset: provenance.rowOffset,
494
+ batchRowCount: provenance.batchRowCount,
495
+ maxFeatures: provenance.maxFeatures,
496
+ };
497
+ }
498
+ /**
499
+ * Walk a batch as a sequence of bounded {@link ColumnarResult} pages.
500
+ *
501
+ * This is how a caller converts more of a batch than one window without raising
502
+ * the ceiling: each page is a complete, independently valid `Result` bounded by
503
+ * `maxFeatures`, and only the page a consumer is holding is live. A consumer
504
+ * that streams a million-row batch to a file therefore retains one page, not a
505
+ * million features.
506
+ *
507
+ * Pages are emitted in ascending row order, contiguous and non-overlapping, so
508
+ * concatenating every page's `features` reproduces exactly the feature sequence
509
+ * a single window over the same range would have produced. An empty range
510
+ * yields no pages at all rather than one empty page.
511
+ *
512
+ * Cancellation is cooperative and real: `options.signal` is checked before each
513
+ * page and every 1,024 rows inside a page, and the traversal yields to the host
514
+ * task queue between pages so an abort raised by a task is actually delivered.
515
+ * An aborted traversal rejects with an `AbortError` `DOMException` and stops
516
+ * without materializing the remaining pages.
517
+ *
518
+ * @throws HonuaGeoArrowError `invalid-input` when `pageSize` is not a positive
519
+ * safe integer, and `row-limit-exceeded` when `pageSize` exceeds `maxFeatures`.
520
+ * Both are checked before the batch is inspected.
521
+ *
522
+ * @example
523
+ * ```ts
524
+ * for await (const page of columnarBatchToResultPages(batch, { pageSize: 1_000, maxFeatures: 1_000, signal })) {
525
+ * await writeRows(page.features);
526
+ * }
527
+ * ```
528
+ */
529
+ export async function* columnarBatchToResultPages(batch, options) {
530
+ const batchRowCount = assertConvertibleBatch(batch, "columnarBatchToResultPages");
531
+ if (typeof options !== "object" || options === null) {
532
+ fail("invalid-input", "columnarBatchToResultPages requires an options object carrying the maxFeatures ceiling.");
533
+ }
534
+ const label = "columnarBatchToResultPages";
535
+ const maxFeatures = requireCeiling(options.maxFeatures, label);
536
+ const pageSize = options.pageSize ?? maxFeatures;
537
+ if (!Number.isSafeInteger(pageSize) || pageSize <= 0) {
538
+ fail("invalid-input", `${label} pageSize must be a positive safe integer.`, {
539
+ resource: "pageSize",
540
+ actual: pageSize,
541
+ });
542
+ }
543
+ if (pageSize > maxFeatures) {
544
+ fail("row-limit-exceeded", `${label} pageSize ${pageSize} exceeds the maxFeatures ceiling of ${maxFeatures}.`, {
545
+ resource: "maxFeatures",
546
+ requested: pageSize,
547
+ limit: maxFeatures,
548
+ });
549
+ }
550
+ const range = resolveRange(batchRowCount, options, label);
551
+ // Refuse a signal that is already aborted before inspecting anything, so a
552
+ // cancelled traversal costs no more than an over-ceiling one.
553
+ throwIfAborted(options.signal);
554
+ if (range.count === 0)
555
+ return;
556
+ const prepared = prepareConversion(batch, options.limits ?? {});
557
+ const telemetry = options.telemetry;
558
+ const end = range.offset + range.count;
559
+ for (let offset = range.offset; offset < end; offset += pageSize) {
560
+ throwIfAborted(options.signal);
561
+ const window = { offset, count: Math.min(pageSize, end - offset) };
562
+ // One span per page: each page is an independently bounded conversion, so
563
+ // the observation matches the unit of work rather than the traversal.
564
+ const span = telemetry
565
+ ? beginColumnarSpan(telemetry, "columnar-result-conversion", batch.identity, {
566
+ batchId: batch.id,
567
+ maxFeatures,
568
+ })
569
+ : undefined;
570
+ let page;
571
+ try {
572
+ const features = await convertRowsCooperatively(prepared, window.offset, window.count, options.signal);
573
+ page = buildResult(prepared, features, window, maxFeatures);
574
+ }
575
+ catch (error) {
576
+ span?.fail(error, error instanceof HonuaGeoArrowError ? { code: error.code } : undefined);
577
+ throw error;
578
+ }
579
+ span?.finish(conversionSpanDetail(page));
580
+ yield page;
581
+ // Between pages, not before the first one: a single-page traversal should
582
+ // not pay a task hop, and after the last page there is nothing to cancel.
583
+ if (offset + pageSize < end)
584
+ await yieldToHost();
585
+ }
586
+ }
587
+ function requiredOption(value, name, predicate) {
588
+ if (value === undefined || !predicate(value)) {
589
+ fail("invalid-input", `resultToColumnarBatch requires options.${name}; only a Result produced by columnarBatchToResult can default it.`, { resource: name });
590
+ }
591
+ return value;
592
+ }
593
+ /** First position of a GeoJSON coordinate tree, used only to infer dimensions. */
594
+ function firstPosition(kind, coordinates) {
595
+ if (kind === "point")
596
+ return coordinates.length === 0 ? undefined : coordinates;
597
+ if (kind === "linestring")
598
+ return Array.isArray(coordinates[0]) ? coordinates[0] : undefined;
599
+ const ring = coordinates[0];
600
+ if (!Array.isArray(ring))
601
+ return undefined;
602
+ return Array.isArray(ring[0]) ? ring[0] : undefined;
603
+ }
604
+ function inferDimensions(position, index) {
605
+ if (position.length === 2)
606
+ return "xy";
607
+ if (position.length === 3)
608
+ return "xyz";
609
+ fail("invalid-input", `result.features[${index}].geometry carries ${position.length}-component positions; declare geometry.dimensions for a layout GeoJSON cannot imply.`, { index, components: position.length });
610
+ }
611
+ /**
612
+ * Convert a bounded, contiguous window of an object-path {@link Result} into a
613
+ * normative GeoArrow columnar batch.
614
+ *
615
+ * This is the inverse of {@link columnarBatchToResult} and is bounded the same
616
+ * way: `maxFeatures` is required and enforced before any feature is read. A
617
+ * `Result` produced by {@link columnarBatchToResult} carries enough provenance
618
+ * to default the batch identity, geometry layout, and attribute bindings; a
619
+ * plain object-path `Result` must supply `id`, `schemaId`, and `identity`, and
620
+ * any attribute it wants lifted into a typed column.
621
+ *
622
+ * Null and missing attributes are treated identically for the nullable
623
+ * timestamp and dictionary columns and become null column values. A feature-id
624
+ * column is not nullable, so a missing or null id fails closed.
625
+ *
626
+ * Unlike the forward direction this one can be asked to lose something: a
627
+ * columnar batch has four column roles and an object `Result` may carry any
628
+ * number of attributes. An attribute no binding covers therefore fails closed
629
+ * by default, and `unmappedAttributes: "drop"` is the only way past it —
630
+ * reporting every dropped name on
631
+ * {@link ResultToColumnarBatchResult.droppedAttributes} so the loss is stated
632
+ * rather than assumed.
633
+ *
634
+ * @throws HonuaGeoArrowError `row-limit-exceeded` when the window exceeds
635
+ * `options.maxFeatures`, checked before any feature is read.
636
+ * @throws HonuaGeoArrowError `unsupported-layout` when the window mixes
637
+ * geometry kinds, which a single columnar geometry column cannot carry, or
638
+ * carries an attribute no column binding covers.
639
+ */
640
+ export function resultToColumnarBatch(result, options) {
641
+ if (typeof result !== "object" || result === null)
642
+ fail("invalid-input", "result must be an object Result.");
643
+ if (typeof options !== "object" || options === null) {
644
+ fail("invalid-input", "resultToColumnarBatch requires an options object carrying the maxFeatures ceiling.");
645
+ }
646
+ const features = result.features;
647
+ if (!Array.isArray(features))
648
+ fail("invalid-input", "result.features must be an array.");
649
+ // Bound first. Nothing below this line runs for an over-ceiling request.
650
+ const window = resolveWindow(features.length, options, "resultToColumnarBatch");
651
+ const source = result;
652
+ const provenance = source.columnar;
653
+ const id = requiredOption(options.id ?? provenance?.batchId, "id", (value) => typeof value === "string" && value !== "");
654
+ const schemaId = requiredOption(options.schemaId ?? provenance?.schemaId, "schemaId", (value) => typeof value === "string" && value !== "");
655
+ const identity = requiredOption(options.identity ?? provenance?.identity, "identity", (value) => typeof value === "object" && value !== null);
656
+ const sequence = options.sequence ?? provenance?.sequence ?? 0;
657
+ const rowOffset = options.rowOffset ?? (provenance === undefined ? undefined : provenance.rowOffset + window.offset);
658
+ const featureIdBinding = options.attributes?.featureId ?? provenance?.attributes.featureId;
659
+ const temporalBinding = options.attributes?.temporal ?? provenance?.attributes.temporal;
660
+ const dictionaryBinding = options.attributes?.dictionary ?? provenance?.attributes.dictionary;
661
+ const unmappedAttributes = options.unmappedAttributes ?? "fail";
662
+ if (unmappedAttributes !== "fail" && unmappedAttributes !== "drop") {
663
+ fail("invalid-input", 'resultToColumnarBatch unmappedAttributes must be "fail" or "drop".', {
664
+ resource: "unmappedAttributes",
665
+ actual: unmappedAttributes,
666
+ });
667
+ }
668
+ const bound = new Set([featureIdBinding?.attribute, temporalBinding?.attribute, dictionaryBinding?.attribute].filter((attribute) => attribute !== undefined));
669
+ const dropped = new Set();
670
+ let kind = options.geometry?.kind ?? provenance?.geometry.kind;
671
+ let dimensions = options.geometry?.dimensions ?? provenance?.geometry.dimensions;
672
+ const values = [];
673
+ const featureIdValues = [];
674
+ const temporalValues = [];
675
+ const dictionaryValues = [];
676
+ const end = window.offset + window.count;
677
+ for (let index = window.offset; index < end; index += 1) {
678
+ const feature = features[index];
679
+ if (typeof feature !== "object" || feature === null) {
680
+ fail("invalid-input", `result.features[${index}] must be a feature object.`, { index });
681
+ }
682
+ // Account for every attribute before reading any of them, so a value the
683
+ // batch cannot carry is refused rather than quietly left behind.
684
+ const attributes = feature.attributes;
685
+ if (attributes !== undefined) {
686
+ if (typeof attributes !== "object" || attributes === null) {
687
+ fail("invalid-input", `result.features[${index}].attributes must be an object.`, { index });
688
+ }
689
+ for (const attribute of Object.keys(attributes)) {
690
+ if (bound.has(attribute))
691
+ continue;
692
+ if (unmappedAttributes === "drop") {
693
+ dropped.add(attribute);
694
+ continue;
695
+ }
696
+ fail("unsupported-layout", `result.features[${index}].attributes.${attribute} has no columnar column; bind it through options.attributes or set unmappedAttributes: "drop" to discard it explicitly.`, { index, attribute });
697
+ }
698
+ }
699
+ if (featureIdBinding !== undefined) {
700
+ const raw = feature.attributes?.[featureIdBinding.attribute];
701
+ if (typeof raw !== "number") {
702
+ fail("invalid-input", `result.features[${index}].attributes.${featureIdBinding.attribute} must be a uint32 feature id; a feature-id column is not nullable.`, { index, attribute: featureIdBinding.attribute });
703
+ }
704
+ featureIdValues.push(raw);
705
+ }
706
+ if (temporalBinding !== undefined) {
707
+ const raw = feature.attributes?.[temporalBinding.attribute];
708
+ if (raw === null || raw === undefined)
709
+ temporalValues.push(null);
710
+ else if (typeof raw === "bigint")
711
+ temporalValues.push(raw);
712
+ else if (typeof raw === "number" && Number.isSafeInteger(raw))
713
+ temporalValues.push(BigInt(raw));
714
+ else {
715
+ fail("invalid-input", `result.features[${index}].attributes.${temporalBinding.attribute} must be a bigint, a safe-integer number in ${temporalBinding.unit}, or null.`, { index, attribute: temporalBinding.attribute });
716
+ }
717
+ }
718
+ if (dictionaryBinding !== undefined) {
719
+ const raw = feature.attributes?.[dictionaryBinding.attribute];
720
+ if (raw === null || raw === undefined)
721
+ dictionaryValues.push(null);
722
+ else if (typeof raw === "string")
723
+ dictionaryValues.push(raw);
724
+ else {
725
+ fail("invalid-input", `result.features[${index}].attributes.${dictionaryBinding.attribute} must be a string or null.`, { index, attribute: dictionaryBinding.attribute });
726
+ }
727
+ }
728
+ const raw = feature.geometry;
729
+ if (raw === null || raw === undefined) {
730
+ values.push(null);
731
+ continue;
732
+ }
733
+ if (typeof raw !== "object") {
734
+ fail("invalid-input", `result.features[${index}].geometry must be a GeoJSON geometry object or null.`, { index });
735
+ }
736
+ const type = raw.type;
737
+ if (typeof type !== "string") {
738
+ fail("invalid-input", `result.features[${index}].geometry must carry a GeoJSON "type"; the columnar path does not accept Esri geometry envelopes.`, { index });
739
+ }
740
+ const featureKind = KIND_BY_GEOJSON_TYPE[type];
741
+ if (featureKind === undefined) {
742
+ fail("unsupported-layout", `Unsupported GeoJSON geometry type "${type}"; a Honua columnar geometry column carries point, linestring, or polygon.`, { index, type });
743
+ }
744
+ if (kind === undefined)
745
+ kind = featureKind;
746
+ else if (kind !== featureKind) {
747
+ fail("unsupported-layout", `result.features[${index}].geometry is "${type}" but the column is geoarrow.${kind}; one columnar geometry column carries exactly one kind.`, { index, expected: kind, actual: featureKind });
748
+ }
749
+ const coordinates = raw.coordinates;
750
+ if (!Array.isArray(coordinates)) {
751
+ fail("invalid-input", `result.features[${index}].geometry.coordinates must be an array.`, { index });
752
+ }
753
+ if (dimensions === undefined) {
754
+ const position = firstPosition(featureKind, coordinates);
755
+ if (position !== undefined)
756
+ dimensions = inferDimensions(position, index);
757
+ }
758
+ values.push(coordinates);
759
+ }
760
+ if (kind === undefined) {
761
+ fail("invalid-input", "resultToColumnarBatch requires options.geometry.kind when the window carries no geometry to infer it from.", { resource: "geometry.kind" });
762
+ }
763
+ // Every geometry in the window is null, so no coordinate layout is
764
+ // observable; xy is the narrowest layout that round-trips those nulls.
765
+ const resolvedDimensions = dimensions ?? "xy";
766
+ const coordinateLayout = options.geometry?.coordinateLayout ?? provenance?.geometry.coordinateLayout;
767
+ const edges = options.geometry?.edges ?? provenance?.geometry.edges;
768
+ const crs = options.geometry?.crs ?? source.crs;
769
+ const base = {
770
+ field: options.geometry?.field ?? provenance?.geometry.field ?? "geometry",
771
+ dimensions: resolvedDimensions,
772
+ ...(coordinateLayout === undefined ? {} : { coordinateLayout }),
773
+ ...(edges === undefined ? {} : { edges }),
774
+ ...(crs === undefined ? {} : { crs }),
775
+ };
776
+ const geometry = kind === "point"
777
+ ? { ...base, kind: "point", values: values }
778
+ : kind === "linestring"
779
+ ? { ...base, kind: "linestring", values: values }
780
+ : { ...base, kind: "polygon", values: values };
781
+ const input = {
782
+ id,
783
+ sequence,
784
+ ...(rowOffset === undefined ? {} : { rowOffset }),
785
+ schemaId,
786
+ identity,
787
+ geometry,
788
+ ...(temporalBinding === undefined
789
+ ? {}
790
+ : {
791
+ temporal: {
792
+ field: temporalBinding.attribute,
793
+ unit: temporalBinding.unit,
794
+ ...(temporalBinding.timezone === undefined ? {} : { timezone: temporalBinding.timezone }),
795
+ values: temporalValues,
796
+ },
797
+ }),
798
+ ...(dictionaryBinding === undefined
799
+ ? {}
800
+ : {
801
+ dictionary: {
802
+ field: dictionaryBinding.attribute,
803
+ ordered: dictionaryBinding.ordered ?? false,
804
+ values: dictionaryValues,
805
+ },
806
+ }),
807
+ ...(featureIdBinding === undefined
808
+ ? {}
809
+ : { featureIds: { field: featureIdBinding.attribute, values: featureIdValues } }),
810
+ };
811
+ const created = createGeoArrowBatch(input, options.limits ?? {});
812
+ // Sorted so two runs over the same window report the same list; the set is
813
+ // always empty unless the caller explicitly opted in to dropping.
814
+ return Object.freeze({ ...created, droppedAttributes: Object.freeze([...dropped].sort()) });
815
+ }