Skip to main content

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;