packed_spatial_index/stream/mod.rs
1//! Streaming reader for the packed spatial index binary format.
2//!
3//! Where [`Index2D::from_bytes`](crate::Index2D::from_bytes) needs the whole
4//! serialized index in memory, the streaming reader answers queries by fetching
5//! only the byte ranges a traversal actually touches, over a [`RangeReader`].
6//! That backing store can be a local file ([`FileReader`]), an in-memory buffer
7//! ([`SliceReader`]), or — by implementing the one-method [`RangeReader`] trait
8//! — a remote object served through HTTP range requests.
9//!
10//! [`open`](StreamIndex2D::open) validates the header and level bounds and
11//! prefetches the small upper levels of the tree (the "directory"). A range
12//! query then descends the tree level by level, fetching each level's boxes
13//! from the directory or in coalesced reads, so it touches only the lower levels
14//! and the few leaf runs the query actually overlaps. [`StreamIndex2D`] and
15//! [`StreamIndex3D`] expose `search` / `search_into` / `visit`, and — when the
16//! index carries a payload section — `search_payloads` / `visit_payloads`, which
17//! also stream each matching item's stored blob (the payload is laid out in leaf
18//! order, so a query fetches its blobs in coalesced reads).
19//!
20//! Pointers are validated as they are followed, so the reader is safe to point
21//! at untrusted data. Available behind the `stream` feature. See [`RangeReader`]
22//! for implementing a remote (e.g. HTTP range) source.
23
24use crate::geometry::{Box2D, Box3D, Overlaps2D, Overlaps3D};
25use crate::persistence::{read_f32_le_unchecked, read_f64_le_unchecked};
26
27#[cfg(feature = "async")]
28mod async_io;
29mod core;
30mod directory;
31mod error;
32mod limits;
33mod payload;
34mod planner;
35mod readers;
36
37#[cfg(feature = "async")]
38pub use self::async_io::AsyncRangeReader;
39pub use self::core::PayloadPrefix;
40pub(crate) use self::core::{StreamCore, read_index};
41pub use self::directory::StreamDirectory;
42pub use self::error::StreamError;
43pub use self::limits::StreamLimits;
44#[cfg(any(unix, windows))]
45pub use self::readers::FileReader;
46#[cfg(test)]
47use self::readers::unexpected_eof;
48pub use self::readers::{RangeReader, SliceReader};
49
50/// Streaming reader for a 2D `f64` packed spatial index.
51///
52/// Open one over any [`RangeReader`] — a local [`FileReader`], an in-memory
53/// [`SliceReader`], or a custom remote source — and query it by fetching only
54/// the byte ranges a traversal needs, instead of loading the whole serialized
55/// index. [`open`](Self::open) validates the header and level bounds and
56/// prefetches the upper levels of the tree.
57///
58/// Queries are fallible (a backing read can fail; a corrupt index is reported
59/// as [`StreamError::Format`]) and otherwise mirror [`Index2D`](crate::Index2D)
60/// range search. Results are item insertion indices, in traversal order.
61///
62/// # Example
63///
64/// ```
65/// use packed_spatial_index::{Box2D, Index2DBuilder, SliceReader, StreamIndex2D};
66///
67/// // Serialize an index once...
68/// let mut builder = Index2DBuilder::new(2);
69/// builder.add(Box2D::new(0.0, 0.0, 1.0, 1.0));
70/// builder.add(Box2D::new(5.0, 5.0, 6.0, 6.0));
71/// let bytes = builder.finish().unwrap().to_bytes();
72///
73/// // ...then query it through a RangeReader without rebuilding it in memory.
74/// let index = StreamIndex2D::open(SliceReader::new(bytes))?;
75/// assert_eq!(index.search(Box2D::new(0.0, 0.0, 2.0, 2.0))?, vec![0]);
76/// # Ok::<(), packed_spatial_index::StreamError>(())
77/// ```
78pub struct StreamIndex2D<R> {
79 core: StreamCore<R>,
80}
81
82impl<R> StreamIndex2D<R> {
83 /// Split off the reader, keeping the reusable [`StreamDirectory`]. No I/O.
84 pub fn into_directory(self) -> (StreamDirectory, R) {
85 let (parts, reader) = self.core.into_parts();
86 (StreamDirectory { parts }, reader)
87 }
88
89 /// Rebuild a 2D `f64` index from a cached directory and a fresh reader. No
90 /// I/O: the directory reads were paid when it was first opened.
91 pub fn from_directory(dir: &StreamDirectory, reader: R) -> Result<Self, StreamError> {
92 Self::from_directory_with_limits(dir, reader, StreamLimits::default())
93 }
94
95 /// [`from_directory`](Self::from_directory) with per-query [`StreamLimits`].
96 pub fn from_directory_with_limits(
97 dir: &StreamDirectory,
98 reader: R,
99 limits: StreamLimits,
100 ) -> Result<Self, StreamError> {
101 Ok(Self {
102 core: dir.reattach(reader, limits, 2 * 2 * 8)?,
103 })
104 }
105
106 /// Number of indexed items.
107 pub fn num_items(&self) -> usize {
108 self.core.num_items
109 }
110
111 /// Whether the index has no items.
112 pub fn is_empty(&self) -> bool {
113 self.core.num_items == 0
114 }
115
116 /// Packed node size of the index.
117 pub fn node_size(&self) -> usize {
118 self.core.node_size
119 }
120
121 /// Whether this index was written with a payload section.
122 pub fn has_payload(&self) -> bool {
123 self.core.has_payload()
124 }
125}
126
127impl<R: RangeReader> StreamIndex2D<R> {
128 /// Open and validate a 2D `f64` index from `reader`.
129 ///
130 /// Reads and validates the header and level bounds and prefetches the upper
131 /// levels of the tree. Returns [`StreamError::Format`] for a corrupt or
132 /// wrong-variant index and [`StreamError::Io`] for a read failure.
133 pub fn open(reader: R) -> Result<Self, StreamError> {
134 Self::open_with_limits(reader, StreamLimits::default())
135 }
136
137 /// Open with per-query cost [`StreamLimits`]. Every query then aborts with
138 /// [`StreamError::LimitExceeded`] if it would exceed a limit — use this to
139 /// bound a broad query's reads / bytes / results to your environment (e.g. a
140 /// worker's subrequest and memory budgets).
141 pub fn open_with_limits(reader: R, limits: StreamLimits) -> Result<Self, StreamError> {
142 Ok(Self {
143 core: StreamCore::open(reader, 2, 8, limits)?,
144 })
145 }
146
147 /// Total extent of all indexed items, or [`None`] for an empty index.
148 ///
149 /// Read from the cached root box, so this costs no I/O.
150 pub fn extent(&self) -> Option<Box2D> {
151 if self.core.num_items == 0 {
152 return None;
153 }
154 // The root is the final node and always sits in the cached directory.
155 let root = self.core.num_nodes - 1;
156 let bytes = self.core.cached_box_bytes(root)?;
157 Some(parse_box2d(bytes))
158 }
159
160 /// Stream the indices of every item whose box intersects `query`, passing
161 /// each to `visitor`.
162 ///
163 /// Fallible: a read from the backing [`RangeReader`] can fail mid-query, and
164 /// a corrupt index is reported as [`StreamError::Format`]. Items are yielded
165 /// in tree-traversal order, which is not part of the API.
166 pub fn visit<F: FnMut(usize)>(&self, query: Box2D, visitor: F) -> Result<(), StreamError> {
167 self.core
168 .visit_ids(|record| parse_box2d(record).overlaps(query), visitor)
169 }
170
171 /// Stream the indices of every item whose box intersects `query`.
172 pub fn search(&self, query: Box2D) -> Result<Vec<usize>, StreamError> {
173 let mut out = Vec::new();
174 self.search_into(query, &mut out)?;
175 Ok(out)
176 }
177
178 /// Like [`search`](Self::search), but appends into a reused buffer (cleared
179 /// first) to avoid reallocating across queries.
180 pub fn search_into(&self, query: Box2D, out: &mut Vec<usize>) -> Result<(), StreamError> {
181 out.clear();
182 self.visit(query, |index| out.push(index))
183 }
184
185 /// Visit `(item index, payload blob)` for every item intersecting `query`.
186 ///
187 /// The payload section is stored in leaf order, so a spatial query fetches
188 /// its blobs (and their offset table) in coalesced reads — a handful of
189 /// round trips even over a remote source, instead of one per item. The blob
190 /// slice is valid only for the duration of each call. Returns
191 /// [`StreamError::NoPayload`] if the index has no payload section.
192 pub fn visit_payloads<F: FnMut(usize, &[u8])>(
193 &self,
194 query: Box2D,
195 visitor: F,
196 ) -> Result<(), StreamError> {
197 self.core
198 .visit_payloads(|record| parse_box2d(record).overlaps(query), visitor)
199 }
200
201 /// Collect `(item index, payload blob)` for every item intersecting `query`.
202 /// The owning counterpart of [`visit_payloads`](Self::visit_payloads).
203 pub fn search_payloads(&self, query: Box2D) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
204 let mut out = Vec::new();
205 self.visit_payloads(query, |id, blob| out.push((id, blob.to_vec())))?;
206 Ok(out)
207 }
208
209 /// Stream the indices of every item whose box overlaps the region `query` —
210 /// any [`Overlaps2D`] shape (a polygon, triangle, …), not just a box.
211 ///
212 /// Subtrees whose node box falls outside `query` are pruned during the
213 /// streamed descent, so a region query fetches only the leaves it overlaps —
214 /// less data than its bounding box. (Pruning can fragment the coalesced runs,
215 /// so the range-request count is shape-dependent; the bytes always shrink.)
216 pub fn visit_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
217 where
218 Q: Overlaps2D,
219 F: FnMut(usize),
220 {
221 self.core
222 .visit_ids(|record| query.overlaps_box(parse_box2d(record)), visitor)
223 }
224
225 /// Collect the indices of every item whose box overlaps the region `query`.
226 pub fn search_region<Q: Overlaps2D>(&self, query: &Q) -> Result<Vec<usize>, StreamError> {
227 let mut out = Vec::new();
228 self.visit_region(query, |index| out.push(index))?;
229 Ok(out)
230 }
231
232 /// Visit `(item index, payload blob)` for every item whose box overlaps the
233 /// region `query`. Like [`visit_payloads`](Self::visit_payloads) but for a
234 /// custom [`Overlaps2D`] shape; node-box pruning fetches only the leaves the
235 /// region touches.
236 pub fn visit_payloads_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
237 where
238 Q: Overlaps2D,
239 F: FnMut(usize, &[u8]),
240 {
241 self.core
242 .visit_payloads(|record| query.overlaps_box(parse_box2d(record)), visitor)
243 }
244
245 /// Collect `(item index, payload blob)` for every item whose box overlaps the
246 /// region `query`. The owning counterpart of
247 /// [`visit_payloads_region`](Self::visit_payloads_region).
248 pub fn search_payloads_region<Q: Overlaps2D>(
249 &self,
250 query: &Q,
251 ) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
252 let mut out = Vec::new();
253 self.visit_payloads_region(query, |id, blob| out.push((id, blob.to_vec())))?;
254 Ok(out)
255 }
256
257 /// Visit a [`PayloadPrefix`] for every item intersecting `query`: id, leaf
258 /// rank, full payload length, and the first `prefix_len` payload bytes.
259 ///
260 /// Payload bodies past the prefix are not read — lengths come from the
261 /// offset table (or the fixed stride) — so identity prefixes and size
262 /// summaries cost a fraction of [`visit_payloads`](Self::visit_payloads).
263 /// Capture `leaf_rank` values here and feed a page of them to
264 /// [`visit_payloads_at_ranks`](Self::visit_payloads_at_ranks) to fetch full
265 /// payloads later. Returns [`StreamError::NoPayload`] if the index has no
266 /// payload section.
267 ///
268 /// # Example
269 ///
270 /// ```
271 /// use packed_spatial_index::{Box2D, Index2DBuilder, SliceReader, StreamIndex2D};
272 ///
273 /// let mut builder = Index2DBuilder::new(2);
274 /// builder.add(Box2D::new(0.0, 0.0, 1.0, 1.0));
275 /// builder.add(Box2D::new(5.0, 5.0, 6.0, 6.0));
276 /// let index = builder.finish()?;
277 /// let bytes = index.to_bytes_with_payloads(&[
278 /// b"first payload".as_slice(),
279 /// b"second payload".as_slice(),
280 /// ])?;
281 ///
282 /// let stream = StreamIndex2D::open(SliceReader::new(bytes))?;
283 /// let mut headers = Vec::new();
284 /// stream.visit_payload_prefixes(Box2D::new(-1.0, -1.0, 7.0, 7.0), 5, |p| {
285 /// headers.push((p.id, p.leaf_rank, p.payload_len, p.prefix.to_vec()));
286 /// })?;
287 /// assert_eq!(headers.len(), 2);
288 ///
289 /// let page_ranks = [headers[0].1];
290 /// let mut page = Vec::new();
291 /// stream.visit_payloads_at_ranks(&page_ranks, |rank, blob| {
292 /// page.push((rank, blob.to_vec()));
293 /// })?;
294 /// assert_eq!(page[0].0, headers[0].1);
295 /// assert_eq!(page[0].1.len(), headers[0].2);
296 /// # Ok::<(), Box<dyn std::error::Error>>(())
297 /// ```
298 pub fn visit_payload_prefixes<F: FnMut(PayloadPrefix<'_>)>(
299 &self,
300 query: Box2D,
301 prefix_len: usize,
302 visitor: F,
303 ) -> Result<(), StreamError> {
304 self.core.visit_payload_prefixes(
305 |record| parse_box2d(record).overlaps(query),
306 prefix_len,
307 visitor,
308 )
309 }
310
311 /// [`visit_payload_prefixes`](Self::visit_payload_prefixes) for a custom
312 /// [`Overlaps2D`] region; node-box pruning fetches only the leaves the
313 /// region touches.
314 pub fn visit_payload_prefixes_region<Q, F>(
315 &self,
316 query: &Q,
317 prefix_len: usize,
318 visitor: F,
319 ) -> Result<(), StreamError>
320 where
321 Q: Overlaps2D,
322 F: FnMut(PayloadPrefix<'_>),
323 {
324 self.core.visit_payload_prefixes(
325 |record| query.overlaps_box(parse_box2d(record)),
326 prefix_len,
327 visitor,
328 )
329 }
330
331 /// Visit `(leaf rank, payload blob)` for an explicit set of leaf ranks —
332 /// random-access payload reads for ranks captured by a prefix visit. Ranks
333 /// are sorted and deduplicated internally, read in coalesced ascending
334 /// runs, and emitted in ascending rank order. A rank at or past the item
335 /// count fails with [`StreamError::InvalidRank`].
336 pub fn visit_payloads_at_ranks<F: FnMut(usize, &[u8])>(
337 &self,
338 leaf_ranks: &[usize],
339 visitor: F,
340 ) -> Result<(), StreamError> {
341 self.core.visit_payloads_at_ranks(leaf_ranks, visitor)
342 }
343}
344
345/// Parse one 2D box record (`[min_x, min_y, max_x, max_y]` little-endian f64).
346fn parse_box2d(bytes: &[u8]) -> Box2D {
347 Box2D::new(
348 read_f64_le_unchecked(bytes, 0),
349 read_f64_le_unchecked(bytes, 8),
350 read_f64_le_unchecked(bytes, 16),
351 read_f64_le_unchecked(bytes, 24),
352 )
353}
354
355/// Streaming reader for a 3D `f64` packed spatial index.
356///
357/// The 3D counterpart of [`StreamIndex2D`]: it shares the same open, validation,
358/// directory prefetch, and coalesced traversal, differing only in the 48-byte
359/// box record. See [`StreamIndex2D`] for the streaming model.
360pub struct StreamIndex3D<R> {
361 core: StreamCore<R>,
362}
363
364impl<R> StreamIndex3D<R> {
365 /// Split off the reader, keeping the reusable [`StreamDirectory`]. No I/O.
366 pub fn into_directory(self) -> (StreamDirectory, R) {
367 let (parts, reader) = self.core.into_parts();
368 (StreamDirectory { parts }, reader)
369 }
370
371 /// Rebuild a 3D `f64` index from a cached directory and a fresh reader. No I/O.
372 pub fn from_directory(dir: &StreamDirectory, reader: R) -> Result<Self, StreamError> {
373 Self::from_directory_with_limits(dir, reader, StreamLimits::default())
374 }
375
376 /// [`from_directory`](Self::from_directory) with per-query [`StreamLimits`].
377 pub fn from_directory_with_limits(
378 dir: &StreamDirectory,
379 reader: R,
380 limits: StreamLimits,
381 ) -> Result<Self, StreamError> {
382 Ok(Self {
383 core: dir.reattach(reader, limits, 3 * 2 * 8)?,
384 })
385 }
386
387 /// Number of indexed items.
388 pub fn num_items(&self) -> usize {
389 self.core.num_items
390 }
391
392 /// Whether the index has no items.
393 pub fn is_empty(&self) -> bool {
394 self.core.num_items == 0
395 }
396
397 /// Packed node size of the index.
398 pub fn node_size(&self) -> usize {
399 self.core.node_size
400 }
401
402 /// Whether this index was written with a payload section.
403 pub fn has_payload(&self) -> bool {
404 self.core.has_payload()
405 }
406}
407
408impl<R: RangeReader> StreamIndex3D<R> {
409 /// Open and validate a 3D `f64` index from `reader`.
410 pub fn open(reader: R) -> Result<Self, StreamError> {
411 Self::open_with_limits(reader, StreamLimits::default())
412 }
413
414 /// Open with per-query cost [`StreamLimits`]. See
415 /// [`StreamIndex2D::open_with_limits`].
416 pub fn open_with_limits(reader: R, limits: StreamLimits) -> Result<Self, StreamError> {
417 Ok(Self {
418 core: StreamCore::open(reader, 3, 8, limits)?,
419 })
420 }
421
422 /// Total extent of all indexed items, or [`None`] for an empty index.
423 /// Read from the cached root box, so this costs no I/O.
424 pub fn extent(&self) -> Option<Box3D> {
425 if self.core.num_items == 0 {
426 return None;
427 }
428 let root = self.core.num_nodes - 1;
429 let bytes = self.core.cached_box_bytes(root)?;
430 Some(parse_box3d(bytes))
431 }
432
433 /// Stream the indices of every item whose box intersects `query`, passing
434 /// each to `visitor`. Fallible; see [`StreamIndex2D::visit`].
435 pub fn visit<F: FnMut(usize)>(&self, query: Box3D, visitor: F) -> Result<(), StreamError> {
436 self.core
437 .visit_ids(|record| parse_box3d(record).overlaps(query), visitor)
438 }
439
440 /// Stream the indices of every item whose box intersects `query`.
441 pub fn search(&self, query: Box3D) -> Result<Vec<usize>, StreamError> {
442 let mut out = Vec::new();
443 self.search_into(query, &mut out)?;
444 Ok(out)
445 }
446
447 /// Like [`search`](Self::search), but appends into a reused buffer.
448 pub fn search_into(&self, query: Box3D, out: &mut Vec<usize>) -> Result<(), StreamError> {
449 out.clear();
450 self.visit(query, |index| out.push(index))
451 }
452
453 /// Visit `(item index, payload blob)` for every item intersecting `query`.
454 /// See [`StreamIndex2D::visit_payloads`].
455 pub fn visit_payloads<F: FnMut(usize, &[u8])>(
456 &self,
457 query: Box3D,
458 visitor: F,
459 ) -> Result<(), StreamError> {
460 self.core
461 .visit_payloads(|record| parse_box3d(record).overlaps(query), visitor)
462 }
463
464 /// Collect `(item index, payload blob)` for every item intersecting `query`.
465 pub fn search_payloads(&self, query: Box3D) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
466 let mut out = Vec::new();
467 self.visit_payloads(query, |id, blob| out.push((id, blob.to_vec())))?;
468 Ok(out)
469 }
470
471 /// Stream the indices of every item whose box overlaps the region `query` —
472 /// any [`Overlaps3D`] shape (e.g. a [`Frustum3D`](crate::Frustum3D)), not just
473 /// a box. Subtrees outside `query` are pruned during the streamed descent.
474 pub fn visit_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
475 where
476 Q: Overlaps3D,
477 F: FnMut(usize),
478 {
479 self.core
480 .visit_ids(|record| query.overlaps_box(parse_box3d(record)), visitor)
481 }
482
483 /// Collect the indices of every item whose box overlaps the region `query`.
484 pub fn search_region<Q: Overlaps3D>(&self, query: &Q) -> Result<Vec<usize>, StreamError> {
485 let mut out = Vec::new();
486 self.visit_region(query, |index| out.push(index))?;
487 Ok(out)
488 }
489
490 /// Visit `(item index, payload blob)` for every item whose box overlaps the
491 /// region `query`; node-box pruning fetches only the leaves it touches.
492 pub fn visit_payloads_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
493 where
494 Q: Overlaps3D,
495 F: FnMut(usize, &[u8]),
496 {
497 self.core
498 .visit_payloads(|record| query.overlaps_box(parse_box3d(record)), visitor)
499 }
500
501 /// Collect `(item index, payload blob)` for every item whose box overlaps the
502 /// region `query`. The owning counterpart of
503 /// [`visit_payloads_region`](Self::visit_payloads_region).
504 pub fn search_payloads_region<Q: Overlaps3D>(
505 &self,
506 query: &Q,
507 ) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
508 let mut out = Vec::new();
509 self.visit_payloads_region(query, |id, blob| out.push((id, blob.to_vec())))?;
510 Ok(out)
511 }
512
513 /// Visit a [`PayloadPrefix`] for every item intersecting `query`; the 3D
514 /// counterpart of [`StreamIndex2D::visit_payload_prefixes`].
515 pub fn visit_payload_prefixes<F: FnMut(PayloadPrefix<'_>)>(
516 &self,
517 query: Box3D,
518 prefix_len: usize,
519 visitor: F,
520 ) -> Result<(), StreamError> {
521 self.core.visit_payload_prefixes(
522 |record| parse_box3d(record).overlaps(query),
523 prefix_len,
524 visitor,
525 )
526 }
527
528 /// [`visit_payload_prefixes`](Self::visit_payload_prefixes) for a custom
529 /// [`Overlaps3D`] region.
530 pub fn visit_payload_prefixes_region<Q, F>(
531 &self,
532 query: &Q,
533 prefix_len: usize,
534 visitor: F,
535 ) -> Result<(), StreamError>
536 where
537 Q: Overlaps3D,
538 F: FnMut(PayloadPrefix<'_>),
539 {
540 self.core.visit_payload_prefixes(
541 |record| query.overlaps_box(parse_box3d(record)),
542 prefix_len,
543 visitor,
544 )
545 }
546
547 /// Visit `(leaf rank, payload blob)` for an explicit set of leaf ranks; the
548 /// 3D counterpart of [`StreamIndex2D::visit_payloads_at_ranks`].
549 pub fn visit_payloads_at_ranks<F: FnMut(usize, &[u8])>(
550 &self,
551 leaf_ranks: &[usize],
552 visitor: F,
553 ) -> Result<(), StreamError> {
554 self.core.visit_payloads_at_ranks(leaf_ranks, visitor)
555 }
556}
557
558/// Parse one 3D box record (`[min_x, min_y, min_z, max_x, max_y, max_z]` LE f64).
559fn parse_box3d(bytes: &[u8]) -> Box3D {
560 Box3D::new(
561 read_f64_le_unchecked(bytes, 0),
562 read_f64_le_unchecked(bytes, 8),
563 read_f64_le_unchecked(bytes, 16),
564 read_f64_le_unchecked(bytes, 24),
565 read_f64_le_unchecked(bytes, 32),
566 read_f64_le_unchecked(bytes, 40),
567 )
568}
569
570/// Parse one 2D `f32` box record (16 bytes), widening to `f64`. The stored f32
571/// bounds were rounded outward, so the widened box is a superset and search stays
572/// conservative (no misses, a few near-boundary false positives).
573fn parse_box2d_f32(bytes: &[u8]) -> Box2D {
574 Box2D::new(
575 read_f32_le_unchecked(bytes, 0) as f64,
576 read_f32_le_unchecked(bytes, 4) as f64,
577 read_f32_le_unchecked(bytes, 8) as f64,
578 read_f32_le_unchecked(bytes, 12) as f64,
579 )
580}
581
582/// Parse one 3D `f32` box record (24 bytes), widening to `f64` (see
583/// [`parse_box2d_f32`]).
584fn parse_box3d_f32(bytes: &[u8]) -> Box3D {
585 Box3D::new(
586 read_f32_le_unchecked(bytes, 0) as f64,
587 read_f32_le_unchecked(bytes, 4) as f64,
588 read_f32_le_unchecked(bytes, 8) as f64,
589 read_f32_le_unchecked(bytes, 12) as f64,
590 read_f32_le_unchecked(bytes, 16) as f64,
591 read_f32_le_unchecked(bytes, 20) as f64,
592 )
593}
594
595/// Streaming reader for a compact `f32` 2D index — the bytes of
596/// [`SimdIndex2DF32`](crate::SimdIndex2DF32) / [`Index2DF32`](crate::Index2DF32).
597/// Half the box bytes on the wire of [`StreamIndex2D`]; results are a
598/// conservative superset (the stored f32 boxes are rounded outward). Behind the
599/// `stream` feature.
600pub struct StreamIndex2DF32<R> {
601 core: StreamCore<R>,
602}
603
604impl<R> StreamIndex2DF32<R> {
605 /// Split off the reader, keeping the reusable [`StreamDirectory`]. No I/O.
606 pub fn into_directory(self) -> (StreamDirectory, R) {
607 let (parts, reader) = self.core.into_parts();
608 (StreamDirectory { parts }, reader)
609 }
610
611 /// Rebuild a 2D `f32` index from a cached directory and a fresh reader. No I/O.
612 pub fn from_directory(dir: &StreamDirectory, reader: R) -> Result<Self, StreamError> {
613 Self::from_directory_with_limits(dir, reader, StreamLimits::default())
614 }
615
616 /// [`from_directory`](Self::from_directory) with per-query [`StreamLimits`].
617 pub fn from_directory_with_limits(
618 dir: &StreamDirectory,
619 reader: R,
620 limits: StreamLimits,
621 ) -> Result<Self, StreamError> {
622 Ok(Self {
623 core: dir.reattach(reader, limits, 2 * 2 * 4)?,
624 })
625 }
626
627 /// Number of indexed items.
628 pub fn num_items(&self) -> usize {
629 self.core.num_items
630 }
631
632 /// Whether the index has no items.
633 pub fn is_empty(&self) -> bool {
634 self.core.num_items == 0
635 }
636
637 /// Packed node size of the index.
638 pub fn node_size(&self) -> usize {
639 self.core.node_size
640 }
641
642 /// Whether this index was written with a payload section.
643 pub fn has_payload(&self) -> bool {
644 self.core.has_payload()
645 }
646}
647
648impl<R: RangeReader> StreamIndex2DF32<R> {
649 /// Open and validate a 2D `f32` index from `reader`.
650 pub fn open(reader: R) -> Result<Self, StreamError> {
651 Self::open_with_limits(reader, StreamLimits::default())
652 }
653
654 /// Open with per-query cost [`StreamLimits`]. See
655 /// [`StreamIndex2D::open_with_limits`].
656 pub fn open_with_limits(reader: R, limits: StreamLimits) -> Result<Self, StreamError> {
657 Ok(Self {
658 core: StreamCore::open(reader, 2, 4, limits)?,
659 })
660 }
661
662 /// Total extent of all indexed items (widened f32 root box), or `None` when
663 /// empty. Costs no I/O.
664 pub fn extent(&self) -> Option<Box2D> {
665 if self.core.num_items == 0 {
666 return None;
667 }
668 let root = self.core.num_nodes - 1;
669 Some(parse_box2d_f32(self.core.cached_box_bytes(root)?))
670 }
671
672 /// Stream the indices of every item whose (rounded) box intersects `query`.
673 pub fn visit<F: FnMut(usize)>(&self, query: Box2D, visitor: F) -> Result<(), StreamError> {
674 self.core
675 .visit_ids(|r| parse_box2d_f32(r).overlaps(query), visitor)
676 }
677
678 /// Stream the indices of every item whose (rounded) box intersects `query`.
679 pub fn search(&self, query: Box2D) -> Result<Vec<usize>, StreamError> {
680 let mut out = Vec::new();
681 self.search_into(query, &mut out)?;
682 Ok(out)
683 }
684
685 /// Like [`search`](Self::search), into a reused buffer (cleared first).
686 pub fn search_into(&self, query: Box2D, out: &mut Vec<usize>) -> Result<(), StreamError> {
687 out.clear();
688 self.visit(query, |index| out.push(index))
689 }
690
691 /// Visit `(item index, payload blob)` for every item intersecting `query`.
692 pub fn visit_payloads<F: FnMut(usize, &[u8])>(
693 &self,
694 query: Box2D,
695 visitor: F,
696 ) -> Result<(), StreamError> {
697 self.core
698 .visit_payloads(|r| parse_box2d_f32(r).overlaps(query), visitor)
699 }
700
701 /// Collect `(item index, payload blob)` for every item intersecting `query`.
702 pub fn search_payloads(&self, query: Box2D) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
703 let mut out = Vec::new();
704 self.visit_payloads(query, |id, blob| out.push((id, blob.to_vec())))?;
705 Ok(out)
706 }
707
708 /// Stream the indices of every item whose (rounded) box overlaps the region
709 /// `query` — any [`Overlaps2D`] shape. Subtrees outside `query` are pruned.
710 pub fn visit_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
711 where
712 Q: Overlaps2D,
713 F: FnMut(usize),
714 {
715 self.core.visit_ids(
716 |record| query.overlaps_box(parse_box2d_f32(record)),
717 visitor,
718 )
719 }
720
721 /// Collect the indices of every item whose box overlaps the region `query`.
722 pub fn search_region<Q: Overlaps2D>(&self, query: &Q) -> Result<Vec<usize>, StreamError> {
723 let mut out = Vec::new();
724 self.visit_region(query, |index| out.push(index))?;
725 Ok(out)
726 }
727
728 /// Visit `(item index, payload blob)` for every item whose box overlaps the
729 /// region `query`; node-box pruning fetches only the leaves it touches.
730 pub fn visit_payloads_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
731 where
732 Q: Overlaps2D,
733 F: FnMut(usize, &[u8]),
734 {
735 self.core.visit_payloads(
736 |record| query.overlaps_box(parse_box2d_f32(record)),
737 visitor,
738 )
739 }
740
741 /// Collect `(item index, payload blob)` for every item whose box overlaps the
742 /// region `query`.
743 pub fn search_payloads_region<Q: Overlaps2D>(
744 &self,
745 query: &Q,
746 ) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
747 let mut out = Vec::new();
748 self.visit_payloads_region(query, |id, blob| out.push((id, blob.to_vec())))?;
749 Ok(out)
750 }
751
752 /// Visit a [`PayloadPrefix`] for every item whose (rounded) box intersects
753 /// `query`; the `f32` counterpart of
754 /// [`StreamIndex2D::visit_payload_prefixes`].
755 pub fn visit_payload_prefixes<F: FnMut(PayloadPrefix<'_>)>(
756 &self,
757 query: Box2D,
758 prefix_len: usize,
759 visitor: F,
760 ) -> Result<(), StreamError> {
761 self.core.visit_payload_prefixes(
762 |record| parse_box2d_f32(record).overlaps(query),
763 prefix_len,
764 visitor,
765 )
766 }
767
768 /// [`visit_payload_prefixes`](Self::visit_payload_prefixes) for a custom
769 /// [`Overlaps2D`] region.
770 pub fn visit_payload_prefixes_region<Q, F>(
771 &self,
772 query: &Q,
773 prefix_len: usize,
774 visitor: F,
775 ) -> Result<(), StreamError>
776 where
777 Q: Overlaps2D,
778 F: FnMut(PayloadPrefix<'_>),
779 {
780 self.core.visit_payload_prefixes(
781 |record| query.overlaps_box(parse_box2d_f32(record)),
782 prefix_len,
783 visitor,
784 )
785 }
786
787 /// Visit `(leaf rank, payload blob)` for an explicit set of leaf ranks; the
788 /// `f32` counterpart of [`StreamIndex2D::visit_payloads_at_ranks`].
789 pub fn visit_payloads_at_ranks<F: FnMut(usize, &[u8])>(
790 &self,
791 leaf_ranks: &[usize],
792 visitor: F,
793 ) -> Result<(), StreamError> {
794 self.core.visit_payloads_at_ranks(leaf_ranks, visitor)
795 }
796}
797
798/// Streaming reader for a compact `f32` 3D index. The 3D counterpart of
799/// [`StreamIndex2DF32`] (24-byte box record). Behind the `stream` feature.
800pub struct StreamIndex3DF32<R> {
801 core: StreamCore<R>,
802}
803
804impl<R> StreamIndex3DF32<R> {
805 /// Split off the reader, keeping the reusable [`StreamDirectory`]. No I/O.
806 pub fn into_directory(self) -> (StreamDirectory, R) {
807 let (parts, reader) = self.core.into_parts();
808 (StreamDirectory { parts }, reader)
809 }
810
811 /// Rebuild a 3D `f32` index from a cached directory and a fresh reader. No I/O.
812 pub fn from_directory(dir: &StreamDirectory, reader: R) -> Result<Self, StreamError> {
813 Self::from_directory_with_limits(dir, reader, StreamLimits::default())
814 }
815
816 /// [`from_directory`](Self::from_directory) with per-query [`StreamLimits`].
817 pub fn from_directory_with_limits(
818 dir: &StreamDirectory,
819 reader: R,
820 limits: StreamLimits,
821 ) -> Result<Self, StreamError> {
822 Ok(Self {
823 core: dir.reattach(reader, limits, 3 * 2 * 4)?,
824 })
825 }
826
827 /// Number of indexed items.
828 pub fn num_items(&self) -> usize {
829 self.core.num_items
830 }
831
832 /// Whether the index has no items.
833 pub fn is_empty(&self) -> bool {
834 self.core.num_items == 0
835 }
836
837 /// Packed node size of the index.
838 pub fn node_size(&self) -> usize {
839 self.core.node_size
840 }
841
842 /// Whether this index was written with a payload section.
843 pub fn has_payload(&self) -> bool {
844 self.core.has_payload()
845 }
846}
847
848impl<R: RangeReader> StreamIndex3DF32<R> {
849 /// Open and validate a 3D `f32` index from `reader`.
850 pub fn open(reader: R) -> Result<Self, StreamError> {
851 Self::open_with_limits(reader, StreamLimits::default())
852 }
853
854 /// Open with per-query cost [`StreamLimits`].
855 pub fn open_with_limits(reader: R, limits: StreamLimits) -> Result<Self, StreamError> {
856 Ok(Self {
857 core: StreamCore::open(reader, 3, 4, limits)?,
858 })
859 }
860
861 /// Total extent (widened f32 root box), or `None` when empty. Costs no I/O.
862 pub fn extent(&self) -> Option<Box3D> {
863 if self.core.num_items == 0 {
864 return None;
865 }
866 let root = self.core.num_nodes - 1;
867 Some(parse_box3d_f32(self.core.cached_box_bytes(root)?))
868 }
869
870 /// Stream the indices of every item whose (rounded) box intersects `query`.
871 pub fn visit<F: FnMut(usize)>(&self, query: Box3D, visitor: F) -> Result<(), StreamError> {
872 self.core
873 .visit_ids(|r| parse_box3d_f32(r).overlaps(query), visitor)
874 }
875
876 /// Stream the indices of every item whose (rounded) box intersects `query`.
877 pub fn search(&self, query: Box3D) -> Result<Vec<usize>, StreamError> {
878 let mut out = Vec::new();
879 self.search_into(query, &mut out)?;
880 Ok(out)
881 }
882
883 /// Like [`search`](Self::search), into a reused buffer (cleared first).
884 pub fn search_into(&self, query: Box3D, out: &mut Vec<usize>) -> Result<(), StreamError> {
885 out.clear();
886 self.visit(query, |index| out.push(index))
887 }
888
889 /// Visit `(item index, payload blob)` for every item intersecting `query`.
890 pub fn visit_payloads<F: FnMut(usize, &[u8])>(
891 &self,
892 query: Box3D,
893 visitor: F,
894 ) -> Result<(), StreamError> {
895 self.core
896 .visit_payloads(|r| parse_box3d_f32(r).overlaps(query), visitor)
897 }
898
899 /// Collect `(item index, payload blob)` for every item intersecting `query`.
900 pub fn search_payloads(&self, query: Box3D) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
901 let mut out = Vec::new();
902 self.visit_payloads(query, |id, blob| out.push((id, blob.to_vec())))?;
903 Ok(out)
904 }
905
906 /// Stream the indices of every item whose (rounded) box overlaps the region
907 /// `query` — any [`Overlaps3D`] shape. Subtrees outside `query` are pruned.
908 pub fn visit_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
909 where
910 Q: Overlaps3D,
911 F: FnMut(usize),
912 {
913 self.core.visit_ids(
914 |record| query.overlaps_box(parse_box3d_f32(record)),
915 visitor,
916 )
917 }
918
919 /// Collect the indices of every item whose box overlaps the region `query`.
920 pub fn search_region<Q: Overlaps3D>(&self, query: &Q) -> Result<Vec<usize>, StreamError> {
921 let mut out = Vec::new();
922 self.visit_region(query, |index| out.push(index))?;
923 Ok(out)
924 }
925
926 /// Visit `(item index, payload blob)` for every item whose box overlaps the
927 /// region `query`; node-box pruning fetches only the leaves it touches.
928 pub fn visit_payloads_region<Q, F>(&self, query: &Q, visitor: F) -> Result<(), StreamError>
929 where
930 Q: Overlaps3D,
931 F: FnMut(usize, &[u8]),
932 {
933 self.core.visit_payloads(
934 |record| query.overlaps_box(parse_box3d_f32(record)),
935 visitor,
936 )
937 }
938
939 /// Collect `(item index, payload blob)` for every item whose box overlaps the
940 /// region `query`.
941 pub fn search_payloads_region<Q: Overlaps3D>(
942 &self,
943 query: &Q,
944 ) -> Result<Vec<(usize, Vec<u8>)>, StreamError> {
945 let mut out = Vec::new();
946 self.visit_payloads_region(query, |id, blob| out.push((id, blob.to_vec())))?;
947 Ok(out)
948 }
949
950 /// Visit a [`PayloadPrefix`] for every item whose (rounded) box intersects
951 /// `query`; the `f32` 3D counterpart of
952 /// [`StreamIndex2D::visit_payload_prefixes`].
953 pub fn visit_payload_prefixes<F: FnMut(PayloadPrefix<'_>)>(
954 &self,
955 query: Box3D,
956 prefix_len: usize,
957 visitor: F,
958 ) -> Result<(), StreamError> {
959 self.core.visit_payload_prefixes(
960 |record| parse_box3d_f32(record).overlaps(query),
961 prefix_len,
962 visitor,
963 )
964 }
965
966 /// [`visit_payload_prefixes`](Self::visit_payload_prefixes) for a custom
967 /// [`Overlaps3D`] region.
968 pub fn visit_payload_prefixes_region<Q, F>(
969 &self,
970 query: &Q,
971 prefix_len: usize,
972 visitor: F,
973 ) -> Result<(), StreamError>
974 where
975 Q: Overlaps3D,
976 F: FnMut(PayloadPrefix<'_>),
977 {
978 self.core.visit_payload_prefixes(
979 |record| query.overlaps_box(parse_box3d_f32(record)),
980 prefix_len,
981 visitor,
982 )
983 }
984
985 /// Visit `(leaf rank, payload blob)` for an explicit set of leaf ranks; the
986 /// `f32` 3D counterpart of [`StreamIndex2D::visit_payloads_at_ranks`].
987 pub fn visit_payloads_at_ranks<F: FnMut(usize, &[u8])>(
988 &self,
989 leaf_ranks: &[usize],
990 visitor: F,
991 ) -> Result<(), StreamError> {
992 self.core.visit_payloads_at_ranks(leaf_ranks, visitor)
993 }
994}
995
996#[cfg(test)]
997mod tests;