Skip to main content

geometry_rtree/
rtree.rs

1//! The [`Rtree`] itself — insert, spatial query, nearest-neighbour, and
2//! Sort-Tile-Recursive bulk load.
3//!
4//! Mirrors `boost/geometry/index/rtree.hpp` and the visitor family under
5//! `index/detail/rtree/visitors/`. Insert is the recursive
6//! least-enlargement descent of `visitors/insert.hpp`; query is the
7//! pruning walk of `visitors/spatial_query.hpp`; nearest is the
8//! best-first search of `visitors/distance_query.hpp`.
9
10use alloc::vec::Vec;
11use core::marker::PhantomData;
12
13use crate::bounds::Bounds;
14use crate::indexable::Indexable;
15use crate::nearest_bound::NearestBound;
16use crate::nearest_iter::NearestIter;
17use crate::node::Node;
18use crate::predicate::{Predicate, QueryPredicate};
19use crate::query_iter::{QueryIter, QueryWithIter};
20use crate::search_frontier::SearchFrontier;
21use crate::split::{AsymmetricRStarSplit, SplitParameters};
22use crate::values::Values;
23
24/// A spatial index over `Indexable` values, parameterised by a split
25/// strategy.
26///
27/// Mirrors `boost::geometry::index::rtree<Value, Parameters>`
28/// (`index/rtree.hpp`). The default uses six-child branches and
29/// 12-value leaves for insertion, with four-child branches and
30/// four-value leaves for bulk packing, via [`AsymmetricRStarSplit`]; pass a symmetric
31/// [`RStarSplit`](crate::split::RStarSplit),
32/// [`Quadratic`](crate::split::Quadratic), or
33/// [`Linear`](crate::split::Linear) as `Params` for a different
34/// trade-off. Most users should retain the default; the [`split`](crate::split)
35/// module explains the parameter order, validity constraints, tuning process,
36/// and benchmark evidence.
37///
38/// # Examples
39///
40/// ```
41/// use geometry_cs::Cartesian;
42/// use geometry_model::Point2D;
43/// use geometry_rtree::Rtree;
44///
45/// type P = Point2D<f64, Cartesian>;
46/// let mut tree: Rtree<P> = Rtree::new();
47/// tree.insert(P::new(1.0, 1.0));
48/// tree.insert(P::new(5.0, 5.0));
49/// assert_eq!(tree.len(), 2);
50/// ```
51#[derive(Debug)]
52pub struct Rtree<T: Indexable, Params: SplitParameters = AsymmetricRStarSplit<6, 2, 12, 4, 4, 4>> {
53    root: Node<T>,
54    len: usize,
55    height: usize,
56    _params: PhantomData<Params>,
57}
58
59impl<T: Indexable, Params: SplitParameters> Default for Rtree<T, Params> {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl<T: Indexable + Clone, Params: SplitParameters> Clone for Rtree<T, Params> {
66    fn clone(&self) -> Self {
67        Self {
68            root: self.root.clone(),
69            len: self.len,
70            height: self.height,
71            _params: PhantomData,
72        }
73    }
74}
75
76impl<T: Indexable, Params: SplitParameters> Rtree<T, Params> {
77    /// An empty tree.
78    #[must_use]
79    pub fn new() -> Self {
80        Self {
81            root: Node::Leaf(Vec::new()),
82            len: 0,
83            height: 1,
84            _params: PhantomData,
85        }
86    }
87
88    /// Number of values in the tree.
89    #[must_use]
90    pub fn len(&self) -> usize {
91        self.len
92    }
93
94    /// Whether the tree holds no values.
95    #[must_use]
96    pub fn is_empty(&self) -> bool {
97        self.len == 0
98    }
99
100    /// The height of the tree (a single-leaf tree is height 1).
101    ///
102    /// This is cached and returned in constant time.
103    #[must_use]
104    pub fn height(&self) -> usize {
105        self.height
106    }
107
108    /// The bounding box covering every value, or `None` for an empty
109    /// tree.
110    #[must_use]
111    #[inline]
112    pub fn bounds(&self) -> Option<Bounds> {
113        self.root.bounds()
114    }
115
116    /// Iterate over every stored value in depth-first tree order.
117    #[must_use]
118    #[inline]
119    pub fn iter(&self) -> Values<'_, T> {
120        Values::new(
121            &self.root,
122            self.len,
123            self.height,
124            Params::BRANCH_MAX.max(Params::BULK_BRANCH_MAX),
125        )
126    }
127
128    /// Remove every value while retaining this tree's split strategy.
129    pub fn clear(&mut self) {
130        self.root = Node::Leaf(Vec::new());
131        self.len = 0;
132        self.height = 1;
133    }
134
135    /// Insert one value.
136    ///
137    /// Descends by least-enlargement to a leaf, inserts, and splits and
138    /// propagates upward if a node overflows. Mirrors
139    /// `visitors/insert.hpp`.
140    pub fn insert(&mut self, value: T) {
141        self.len += 1;
142        if let Some((b1, n1, b2, n2)) = insert_into::<T, Params>(&mut self.root, value) {
143            // The root split into two nodes n1/n2 (which already hold all
144            // the old root's entries); grow a new root one level taller
145            // over them.
146            self.root = Node::Branch(Vec::from([(b1, n1), (b2, n2)]));
147            self.height += 1;
148        }
149    }
150
151    /// Every value whose bounds satisfy `predicate`.
152    ///
153    /// [`query_iter`](Self::query_iter) collected — the lazy walk is
154    /// the crate's single query implementation. Mirrors
155    /// `visitors/spatial_query.hpp`.
156    #[must_use]
157    pub fn query(&self, predicate: Predicate) -> Vec<&T> {
158        self.query_iter(predicate).collect()
159    }
160
161    /// Every value accepted by a logical or user-defined predicate.
162    ///
163    /// This is the extensible companion to [`query`](Self::query).
164    #[must_use]
165    pub fn query_with<P: QueryPredicate<T>>(&self, predicate: P) -> Vec<&T> {
166        self.query_iter_with(predicate).collect()
167    }
168
169    /// Lazily iterate the values whose bounds satisfy `predicate`.
170    ///
171    /// The pruning walk of `visitors/spatial_query.hpp` as a lazy
172    /// iterator: stopping early performs no traversal past the value
173    /// stopped at, and folding stores no output.
174    /// [`query`](Self::query) is this walk collected.
175    ///
176    /// # Examples
177    ///
178    /// Fold without collecting:
179    ///
180    /// ```
181    /// use geometry_rtree::{Bounds, Predicate, Rtree};
182    ///
183    /// let tree: Rtree<(Bounds, u32)> = (0..100u32)
184    ///     .map(|i| (Bounds::point([f64::from(i), 0.0]), i))
185    ///     .collect();
186    /// let window = Predicate::Intersects(Bounds::new([10.0, -1.0], [19.0, 1.0]));
187    /// let id_sum: u32 = tree.query_iter(window).map(|(_, id)| id).sum();
188    /// assert_eq!(id_sum, (10..=19).sum());
189    /// ```
190    #[must_use]
191    pub fn query_iter(&self, predicate: Predicate) -> QueryIter<'_, T> {
192        QueryIter::new(
193            &self.root,
194            predicate,
195            self.height(),
196            Params::BRANCH_MAX.max(Params::BULK_BRANCH_MAX),
197        )
198    }
199
200    /// Lazily iterate values accepted by a logical or user-defined
201    /// predicate.
202    ///
203    /// Built-in spatial predicates should use [`query_iter`](Self::query_iter),
204    /// whose concrete iterator keeps that common path compact.
205    #[must_use]
206    pub fn query_iter_with<P: QueryPredicate<T>>(&self, predicate: P) -> QueryWithIter<'_, T, P> {
207        QueryWithIter::new(
208            &self.root,
209            predicate,
210            self.height(),
211            Params::BRANCH_MAX.max(Params::BULK_BRANCH_MAX),
212        )
213    }
214
215    /// Lazily iterate ALL values, nearest to `query` first — an
216    /// unbounded ordered stream over the entire tree.
217    ///
218    /// The consumer supplies its own bound via
219    /// [`take`](Iterator::take); with no `k` up front nothing can be
220    /// pruned, so a caller who knows `k` and wants maximum pruning
221    /// calls [`nearest`](Self::nearest) instead. Distances are compared
222    /// SQUARED, the same ordering [`nearest`](Self::nearest) uses.
223    ///
224    /// # Examples
225    ///
226    /// Nearest-one:
227    ///
228    /// ```
229    /// use geometry_rtree::{Bounds, Rtree};
230    ///
231    /// let tree: Rtree<(Bounds, u32)> = (0..100u32)
232    ///     .map(|i| (Bounds::point([f64::from(i), 0.0]), i))
233    ///     .collect();
234    /// let (_, nearest_id) = tree.nearest_iter([41.7, 0.0]).next().unwrap();
235    /// assert_eq!(*nearest_id, 42);
236    /// ```
237    ///
238    /// Over-fetch and re-rank: take more than needed by box distance,
239    /// re-rank by a finer key, keep the best:
240    ///
241    /// ```
242    /// use geometry_rtree::{Bounds, Rtree};
243    ///
244    /// let tree: Rtree<(Bounds, u32)> = (0..100u32)
245    ///     .map(|i| (Bounds::point([f64::from(i), 0.0]), i))
246    ///     .collect();
247    /// let mut candidates: Vec<&(Bounds, u32)> =
248    ///     tree.nearest_iter([50.2, 0.0]).take(8).collect();
249    /// candidates.sort_by_key(|(_, id)| *id);
250    /// candidates.truncate(2);
251    /// let ids: Vec<u32> = candidates.iter().map(|(_, id)| *id).collect();
252    /// assert_eq!(ids, [47, 48]);
253    /// ```
254    #[must_use]
255    pub fn nearest_iter(&self, query: [f64; 2]) -> NearestIter<'_, T> {
256        NearestIter::new(&self.root, query)
257    }
258
259    /// Lazily iterate all values nearest-first with caller-selected
260    /// inline capacities for the node and value frontiers.
261    ///
262    /// Entries beyond either capacity spill that frontier to an
263    /// allocated binary heap. Smaller capacities reduce the iterator's
264    /// stack footprint and initialization cost; larger capacities avoid
265    /// spills on wider searches. Prefer [`nearest_iter`](Self::nearest_iter)
266    /// unless measurements for the caller's tree and query distribution
267    /// justify a different pair.
268    #[must_use]
269    pub fn nearest_iter_with_inline_capacities<
270        const NODE_INLINE_CAPACITY: usize,
271        const VALUE_INLINE_CAPACITY: usize,
272    >(
273        &self,
274        query: [f64; 2],
275    ) -> NearestIter<'_, T, NODE_INLINE_CAPACITY, VALUE_INLINE_CAPACITY> {
276        NearestIter::new(&self.root, query)
277    }
278
279    /// The `k` values nearest to the query point, closest first.
280    ///
281    /// Best-first search over node bounding boxes by SQUARED minimum
282    /// possible distance (same ordering as true distance, no square
283    /// roots). A stack-first frontier (`SearchFrontier`) holds
284    /// unexpanded NODES only, popped nearest-first; candidate values
285    /// never enter it. Each candidate instead goes through a bounded
286    /// max-heap (`NearestBound`) whose entries pair each distance with
287    /// its value. Collecting the final values reuses that heap's
288    /// allocation in place, so the whole search performs one
289    /// `min(k, len)`-sized heap allocation unless the frontier spills.
290    ///
291    /// Termination: a child's box is contained in its parent's, so
292    /// frontier pops ascend in minimum possible distance. When a popped
293    /// node's distance reaches the k-th-best value distance, every
294    /// value in every unvisited subtree is at least that far away and
295    /// the held ranks are final; equality only ties distances already
296    /// held. Mirrors `visitors/distance_query.hpp`.
297    ///
298    /// This bounded implementation stays dedicated: its best-k pruning
299    /// needs `k` up front, which the unbounded
300    /// [`nearest_iter`](Self::nearest_iter) stream cannot have.
301    #[must_use]
302    pub fn nearest(&self, query: [f64; 2], k: usize) -> Vec<&T> {
303        if k == 0 || self.len == 0 {
304            return Vec::new();
305        }
306        let capacity = k.min(self.len);
307        let mut ranks = NearestBound::new(k, capacity);
308        let mut frontier: SearchFrontier<FrontierNode<'_, T>> = SearchFrontier::new();
309        frontier.push(FrontierNode {
310            dist: 0.0,
311            node: &self.root,
312        });
313        while let Some(FrontierNode { dist, node }) = frontier.pop() {
314            if dist.total_cmp(&ranks.bound()).is_ge() {
315                break;
316            }
317            match node {
318                Node::Leaf(values) => admit_nearest_values(values.iter(), query, &mut ranks),
319                Node::Branch(children) => {
320                    let bound = ranks.bound();
321                    for (b, child) in children {
322                        let dist = b.comparable_min_distance_to(query);
323                        if dist.total_cmp(&bound).is_lt() {
324                            frontier.push(FrontierNode { dist, node: child });
325                        }
326                    }
327                }
328            }
329        }
330        ranks.into_values()
331    }
332
333    /// Count values equal to `value`.
334    ///
335    /// Mirrors `boost::geometry::index::rtree::count`. Equality is
336    /// evaluated with `T::eq`; the indexable bounds are not used as a
337    /// substitute for value equality.
338    #[must_use]
339    pub fn count(&self, value: &T) -> usize
340    where
341        T: PartialEq,
342    {
343        self.query_iter(Predicate::Intersects(value.bounds()))
344            .filter(|candidate| *candidate == value)
345            .count()
346    }
347
348    /// Remove one value equal to `value`, returning `1` when found and
349    /// `0` otherwise.
350    ///
351    /// Underfull nodes are detached and their remaining values are
352    /// reinserted, then a one-child root is collapsed. This is the
353    /// value-level analogue of Boost's `visitors/remove.hpp` condense
354    /// walk and preserves every configured minimum-fill invariant.
355    #[must_use]
356    pub fn remove(&mut self, value: &T) -> usize
357    where
358        T: PartialEq,
359    {
360        let mut orphans = Vec::new();
361        if !remove_from::<T, Params>(&mut self.root, value, &value.bounds(), &mut orphans) {
362            return 0;
363        }
364
365        self.len -= 1;
366        self.collapse_root();
367        for orphan in orphans {
368            self.insert_without_len(orphan);
369        }
370        debug_assert_eq!(self.root.value_count(), self.len);
371        debug_assert_eq!(self.root.height(), self.height);
372        1
373    }
374
375    /// Remove one occurrence of each supplied value and return the
376    /// number removed.
377    ///
378    /// This is the Rust iterator counterpart of Boost's range-removal
379    /// overload.
380    #[must_use]
381    pub fn remove_all<'a, I>(&mut self, values: I) -> usize
382    where
383        T: PartialEq + 'a,
384        I: IntoIterator<Item = &'a T>,
385    {
386        values.into_iter().map(|value| self.remove(value)).sum()
387    }
388
389    fn insert_without_len(&mut self, value: T) {
390        if let Some((b1, n1, b2, n2)) = insert_into::<T, Params>(&mut self.root, value) {
391            self.root = Node::Branch(Vec::from([(b1, n1), (b2, n2)]));
392            self.height += 1;
393        }
394    }
395
396    fn collapse_root(&mut self) {
397        loop {
398            let replacement = match &mut self.root {
399                Node::Branch(children) if children.is_empty() => Some(Node::Leaf(Vec::new())),
400                Node::Branch(children) if children.len() == 1 => {
401                    Some(children.pop().expect("one root child").1)
402                }
403                Node::Leaf(_) | Node::Branch(_) => None,
404            };
405            let Some(replacement) = replacement else {
406                break;
407            };
408            self.root = replacement;
409            self.height = self.root.height();
410        }
411        if self.len == 0 {
412            self.root = Node::Leaf(Vec::new());
413            self.height = 1;
414        }
415    }
416}
417
418fn remove_from<T, Params>(
419    node: &mut Node<T>,
420    value: &T,
421    value_bounds: &Bounds,
422    orphans: &mut Vec<T>,
423) -> bool
424where
425    T: Indexable + PartialEq,
426    Params: SplitParameters,
427{
428    match node {
429        Node::Leaf(values) => {
430            let Some(index) = values.iter().position(|candidate| candidate == value) else {
431                return false;
432            };
433            values.swap_remove(index);
434            true
435        }
436        Node::Branch(children) => {
437            for index in 0..children.len() {
438                if !children[index].0.contains(value_bounds) {
439                    continue;
440                }
441                if !remove_from::<T, Params>(&mut children[index].1, value, value_bounds, orphans) {
442                    continue;
443                }
444
445                let underfull = match &children[index].1 {
446                    Node::Leaf(values) => values.len() < Params::LEAF_MIN,
447                    Node::Branch(grandchildren) => grandchildren.len() < Params::BRANCH_MIN,
448                };
449                if underfull {
450                    let (_, removed) = children.swap_remove(index);
451                    append_values(removed, orphans);
452                } else {
453                    children[index].0 = children[index]
454                        .1
455                        .bounds()
456                        .expect("a retained child is non-empty");
457                }
458                return true;
459            }
460            false
461        }
462    }
463}
464
465fn append_values<T>(node: Node<T>, values: &mut Vec<T>) {
466    match node {
467        Node::Leaf(mut leaf) => values.append(&mut leaf),
468        Node::Branch(children) => {
469            for (_, child) in children {
470                append_values(child, values);
471            }
472        }
473    }
474}
475
476fn admit_nearest_values<'a, T: Indexable>(
477    values: impl Iterator<Item = &'a T>,
478    query: [f64; 2],
479    ranks: &mut NearestBound<&'a T>,
480) {
481    for value in values {
482        let dist = value.bounds().comparable_min_distance_to(query);
483        if dist.total_cmp(&ranks.bound()).is_lt() {
484            ranks.admit_better(dist, value);
485        }
486    }
487}
488
489impl<T: Indexable, Params: SplitParameters> FromIterator<T> for Rtree<T, Params> {
490    /// Bulk-load with top-down Sort-Tile-Recursive packing: recursively
491    /// partition cached centroids into balanced x/y tiles until they fit
492    /// the configured bulk leaf capacity. Produces a balanced tree, the
493    /// analogue of Boost's `pack_create`
494    /// (`index/detail/rtree/pack_create.hpp`).
495    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
496        let values: Vec<T> = iter.into_iter().collect();
497        let len = values.len();
498        assert!(
499            Params::BULK_LEAF_MAX > 0,
500            "bulk leaf capacity must be non-zero"
501        );
502        assert!(
503            Params::BULK_BRANCH_MAX >= 2,
504            "bulk branch capacity must be at least two"
505        );
506        if len <= Params::BULK_LEAF_MAX {
507            return Self {
508                root: Node::Leaf(values),
509                len,
510                height: 1,
511                _params: PhantomData,
512            };
513        }
514        let (root, height) = str_pack::<T, Params>(values);
515        Self {
516            root,
517            len,
518            height,
519            _params: PhantomData,
520        }
521    }
522}
523
524impl<T: Indexable, Params: SplitParameters> Extend<T> for Rtree<T, Params> {
525    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
526        for value in iter {
527            self.insert(value);
528        }
529    }
530}
531
532impl<'a, T: Indexable, Params: SplitParameters> IntoIterator for &'a Rtree<T, Params> {
533    type Item = &'a T;
534    type IntoIter = Values<'a, T>;
535
536    fn into_iter(self) -> Self::IntoIter {
537        self.iter()
538    }
539}
540
541/// A frontier entry of the best-first nearest search: an unexpanded
542/// node keyed by the minimum possible distance of anything inside it.
543struct FrontierNode<'a, T> {
544    dist: f64,
545    node: &'a Node<T>,
546}
547
548impl<T> PartialEq for FrontierNode<'_, T> {
549    fn eq(&self, other: &Self) -> bool {
550        self.dist.total_cmp(&other.dist).is_eq()
551    }
552}
553
554impl<T> Eq for FrontierNode<'_, T> {}
555
556impl<T> PartialOrd for FrontierNode<'_, T> {
557    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
558        Some(self.cmp(other))
559    }
560}
561
562impl<T> Ord for FrontierNode<'_, T> {
563    /// Reversed so the max-first [`SearchFrontier`] pops the SMALLEST
564    /// distance first.
565    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
566        other.dist.total_cmp(&self.dist)
567    }
568}
569
570/// Recursively insert `value` into `node`. Returns `Some((b1,n1,b2,n2))`
571/// if `node` split, giving the caller the two replacement children.
572type Split<T> = (Bounds, Node<T>, Bounds, Node<T>);
573
574fn insert_into<T: Indexable, Params: SplitParameters>(
575    node: &mut Node<T>,
576    value: T,
577) -> Option<Split<T>> {
578    match node {
579        Node::Leaf(leaf) => {
580            leaf.push(value);
581            if leaf.len() > Params::LEAF_MAX {
582                Some(split_leaf::<T, Params>(leaf))
583            } else {
584                None
585            }
586        }
587        Node::Branch(children) => {
588            // Choose the child that needs the least enlargement.
589            let vb = value.bounds();
590            let choice = choose_child(children, &vb);
591            let (_, child) = &mut children[choice];
592            let split = insert_into::<T, Params>(child, value);
593
594            if let Some((b1, n1, b2, n2)) = split {
595                children[choice] = (b1, n1);
596                children.push((b2, n2));
597                if children.len() > Params::BRANCH_MAX {
598                    return Some(split_branch::<T, Params>(children));
599                }
600            } else {
601                // No split: the child now holds its old contents plus
602                // `value`, so its box is the old box grown by `vb` —
603                // O(1), no subtree walk.
604                children[choice].0 = children[choice].0.union(&vb);
605            }
606            None
607        }
608    }
609}
610
611/// Index of the child whose box enlarges least to admit `vb` (ties
612/// broken by smaller area).
613#[allow(
614    clippy::float_cmp,
615    reason = "exact tie-break between equal enlargements, as Boost's choose_next_node does"
616)]
617fn choose_child<T>(children: &[(Bounds, Node<T>)], vb: &Bounds) -> usize {
618    let mut best = 0;
619    let mut best_enl = f64::INFINITY;
620    let mut best_area = f64::INFINITY;
621    for (i, (b, _)) in children.iter().enumerate() {
622        let enl = b.enlargement(vb);
623        let area = b.area();
624        if enl < best_enl || (enl == best_enl && area < best_area) {
625            best = i;
626            best_enl = enl;
627            best_area = area;
628        }
629    }
630    best
631}
632
633/// Split an overflowing leaf's values into two leaves.
634fn split_leaf<T: Indexable, Params: SplitParameters>(leaf: &mut Vec<T>) -> Split<T> {
635    let taken = core::mem::take(leaf);
636    let boxes: Vec<Bounds> = taken.iter().map(Indexable::bounds).collect();
637    let (g1, g2) = Params::split_leaf(&boxes);
638
639    // Partition `taken` into the two index groups. Walk once, routing by
640    // membership in g1.
641    let mut in_g1 = alloc::vec![false; taken.len()];
642    for &i in &g1 {
643        in_g1[i] = true;
644    }
645    let mut v1: Vec<T> = Vec::new();
646    let mut v2: Vec<T> = Vec::new();
647    for (i, v) in taken.into_iter().enumerate() {
648        if in_g1[i] {
649            v1.push(v);
650        } else {
651            v2.push(v);
652        }
653    }
654    debug_assert_eq!(v1.len(), g1.len());
655    debug_assert_eq!(v2.len(), g2.len());
656
657    let b1 = v1
658        .iter()
659        .map(Indexable::bounds)
660        .reduce(|a, b| a.union(&b))
661        .expect("split group is non-empty by MIN invariant");
662    let b2 = v2
663        .iter()
664        .map(Indexable::bounds)
665        .reduce(|a, b| a.union(&b))
666        .expect("split group is non-empty by MIN invariant");
667    (b1, Node::Leaf(v1), b2, Node::Leaf(v2))
668}
669
670/// Split an overflowing branch's children into two branches.
671fn split_branch<T: Indexable, Params: SplitParameters>(
672    children: &mut Vec<(Bounds, Node<T>)>,
673) -> Split<T> {
674    let taken = core::mem::take(children);
675    let boxes: Vec<Bounds> = taken.iter().map(|(b, _)| *b).collect();
676    let (g1, _g2) = Params::split_branch(&boxes);
677
678    let mut in_g1 = alloc::vec![false; taken.len()];
679    for &i in &g1 {
680        in_g1[i] = true;
681    }
682    let mut c1: Vec<(Bounds, Node<T>)> = Vec::new();
683    let mut c2: Vec<(Bounds, Node<T>)> = Vec::new();
684    for (i, c) in taken.into_iter().enumerate() {
685        if in_g1[i] {
686            c1.push(c);
687        } else {
688            c2.push(c);
689        }
690    }
691
692    let b1 = c1
693        .iter()
694        .map(|(b, _)| *b)
695        .reduce(|a, b| a.union(&b))
696        .expect("split group is non-empty by MIN invariant");
697    let b2 = c2
698        .iter()
699        .map(|(b, _)| *b)
700        .reduce(|a, b| a.union(&b))
701        .expect("split group is non-empty by MIN invariant");
702    (b1, Node::Branch(c1), b2, Node::Branch(c2))
703}
704
705/// Sort-Tile-Recursive packing of `values` into a balanced tree.
706fn str_pack<T: Indexable, Params: SplitParameters>(values: Vec<T>) -> (Node<T>, usize) {
707    // Cache sort keys once: recursive spatial partitioning otherwise
708    // calls `bounds()` throughout every sort comparator.
709    let mut values: Vec<Option<T>> = values.into_iter().map(Some).collect();
710    let keyed: Vec<([f64; 2], usize)> = values
711        .iter()
712        .enumerate()
713        .map(|(index, value)| {
714            (
715                value
716                    .as_ref()
717                    .expect("packed value is present")
718                    .bounds()
719                    .center(),
720                index,
721            )
722        })
723        .collect();
724    let mut height = 1;
725    let mut capacity = Params::BULK_LEAF_MAX;
726    while capacity < keyed.len() {
727        capacity = capacity.saturating_mul(Params::BULK_BRANCH_MAX);
728        height += 1;
729    }
730    (
731        str_pack_height::<T, Params>(keyed, height, &mut values).1,
732        height,
733    )
734}
735
736/// Top-down STR partitioning at one known tree height. Each child gets
737/// a balanced spatial tile small enough for the remaining subtree
738/// capacity, avoiding cross-strip grouping at upper levels.
739fn str_pack_height<T: Indexable, Params: SplitParameters>(
740    mut keyed: Vec<([f64; 2], usize)>,
741    height: usize,
742    values: &mut [Option<T>],
743) -> (Bounds, Node<T>) {
744    if height == 1 {
745        debug_assert!(keyed.len() <= Params::BULK_LEAF_MAX);
746        let leaf_values: Vec<T> = keyed
747            .into_iter()
748            .map(|(_, index)| values[index].take().expect("packed value is present"))
749            .collect();
750        let bounds = leaf_values
751            .iter()
752            .map(Indexable::bounds)
753            .reduce(|a, b| a.union(&b))
754            .expect("packed leaf is non-empty");
755        return (bounds, Node::Leaf(leaf_values));
756    }
757
758    let child_capacity = packed_subtree_capacity::<Params>(height - 1);
759    let child_count = keyed.len().div_ceil(child_capacity);
760    debug_assert!(child_count <= Params::BULK_BRANCH_MAX);
761    let column_count = isqrt_ceil(child_count).max(1);
762
763    let mut children = Vec::with_capacity(child_count);
764    let mut remaining_children = child_count;
765    for column in 0..column_count {
766        let children_in_column =
767            child_count / column_count + usize::from(column < child_count % column_count);
768        // `column_count <= child_count` for every non-empty packed level, so
769        // each column owns at least one child.
770        let base = keyed.len() / remaining_children;
771        let extra = keyed.len() % remaining_children;
772        let take = base * children_in_column + extra.min(children_in_column);
773        let mut strip = take_lowest_by_axis(&mut keyed, take, 0);
774
775        let mut remaining_in_column = children_in_column;
776        while remaining_in_column != 0 {
777            let take = strip.len().div_ceil(remaining_in_column);
778            let tile = take_lowest_by_axis(&mut strip, take, 1);
779            children.push(str_pack_height::<T, Params>(tile, height - 1, values));
780            remaining_in_column -= 1;
781        }
782        remaining_children -= children_in_column;
783    }
784
785    let bounds = children
786        .iter()
787        .map(|(bounds, _)| *bounds)
788        .reduce(|a, b| a.union(&b))
789        .expect("packed branch is non-empty");
790    (bounds, Node::Branch(children))
791}
792
793fn take_lowest_by_axis(
794    values: &mut Vec<([f64; 2], usize)>,
795    take: usize,
796    axis: usize,
797) -> Vec<([f64; 2], usize)> {
798    if take == values.len() {
799        return core::mem::take(values);
800    }
801    values.select_nth_unstable_by(take, |(a, _), (b, _)| a[axis].total_cmp(&b[axis]));
802    let tail = values.split_off(take);
803    core::mem::replace(values, tail)
804}
805
806fn packed_subtree_capacity<Params: SplitParameters>(height: usize) -> usize {
807    let mut capacity = Params::BULK_LEAF_MAX;
808    for _ in 1..height {
809        capacity = capacity.saturating_mul(Params::BULK_BRANCH_MAX);
810    }
811    capacity
812}
813
814/// Ceil of the integer square root, without floating point (MSRV
815/// predates `usize::isqrt`). Integer Newton iteration.
816fn isqrt_ceil(n: usize) -> usize {
817    if n < 2 {
818        return n;
819    }
820    let mut x = n;
821    let mut y = x.div_ceil(2);
822    while y < x {
823        x = y;
824        y = usize::midpoint(x, n / x);
825    }
826    // `x` is floor(sqrt(n)); round up if it is not exact.
827    if x * x == n { x } else { x + 1 }
828}
829
830#[cfg(test)]
831#[allow(clippy::float_cmp, reason = "exact integer-valued point coordinates")]
832mod tests {
833    use super::{FrontierNode, Rtree, isqrt_ceil};
834    use crate::bounds::{Bounds, union_all};
835    use crate::indexable::Indexable;
836    use crate::nearest_bound::{NearestBound, NearestBoundMetrics};
837    use crate::node::Node;
838    use crate::predicate::Predicate;
839    use crate::search_frontier::SearchFrontier;
840    use crate::split::{
841        AsymmetricQuadratic, AsymmetricRStarSplit, Linear, Quadratic, SplitParameters,
842    };
843    use geometry_cs::Cartesian;
844    use geometry_model::Point2D;
845    use geometry_trait::Point as _;
846
847    type P = Point2D<f64, Cartesian>;
848    trait LeafProbe<T> {
849        fn values(&self) -> &[T];
850    }
851
852    impl<T> LeafProbe<T> for Vec<T> {
853        fn values(&self) -> &[T] {
854            self
855        }
856    }
857
858    struct Lcg {
859        state: u64,
860    }
861
862    impl Lcg {
863        fn new() -> Self {
864            Self {
865                state: 0x9E37_79B9_7F4A_7C15,
866            }
867        }
868
869        #[allow(
870            clippy::cast_precision_loss,
871            reason = "state >> 11 keeps 53 bits, exact in f64"
872        )]
873        fn next_f64(&mut self) -> f64 {
874            self.state = self
875                .state
876                .wrapping_mul(6_364_136_223_846_793_005)
877                .wrapping_add(1_442_695_040_888_963_407);
878            (self.state >> 11) as f64 / (1u64 << 53) as f64
879        }
880    }
881
882    #[derive(Debug, Default)]
883    struct BoundedSearchMetrics {
884        frontier_pushes: usize,
885        frontier_pops: usize,
886        frontier_high_water: usize,
887        terminated_by_bound: usize,
888        branch_expansions: usize,
889        child_distance_evaluations: usize,
890        child_order_comparisons: usize,
891        child_pushes: usize,
892        child_pruned: usize,
893        leaf_expansions: usize,
894        reversed_leaf_scans: usize,
895        leaf_group_bound_evaluations: usize,
896        leaf_group_order_comparisons: usize,
897        leaf_groups_scanned: usize,
898        leaf_groups_pruned: usize,
899        value_distance_evaluations: usize,
900        value_bound_passes: usize,
901        value_bound_rejections: usize,
902        rank: NearestBoundMetrics,
903    }
904
905    impl BoundedSearchMetrics {
906        fn add(&mut self, other: &Self) {
907            self.frontier_pushes += other.frontier_pushes;
908            self.frontier_pops += other.frontier_pops;
909            self.frontier_high_water = self.frontier_high_water.max(other.frontier_high_water);
910            self.terminated_by_bound += other.terminated_by_bound;
911            self.branch_expansions += other.branch_expansions;
912            self.child_distance_evaluations += other.child_distance_evaluations;
913            self.child_order_comparisons += other.child_order_comparisons;
914            self.child_pushes += other.child_pushes;
915            self.child_pruned += other.child_pruned;
916            self.leaf_expansions += other.leaf_expansions;
917            self.reversed_leaf_scans += other.reversed_leaf_scans;
918            self.leaf_group_bound_evaluations += other.leaf_group_bound_evaluations;
919            self.leaf_group_order_comparisons += other.leaf_group_order_comparisons;
920            self.leaf_groups_scanned += other.leaf_groups_scanned;
921            self.leaf_groups_pruned += other.leaf_groups_pruned;
922            self.value_distance_evaluations += other.value_distance_evaluations;
923            self.value_bound_passes += other.value_bound_passes;
924            self.value_bound_rejections += other.value_bound_rejections;
925            self.rank.calls += other.rank.calls;
926            self.rank.partition_comparisons += other.rank.partition_comparisons;
927            self.rank.admissions += other.rank.admissions;
928            self.rank.replacements += other.rank.replacements;
929            self.rank.shifted_ranks += other.rank.shifted_ranks;
930        }
931    }
932
933    fn nearest_with_metrics<T: Indexable, Params: SplitParameters>(
934        tree: &Rtree<T, Params>,
935        query: [f64; 2],
936        k: usize,
937        nearer_y_end_first: bool,
938        leaf_group_size: usize,
939        center_out: bool,
940        leaf_bvh_terminal_size: usize,
941    ) -> (Vec<&T>, BoundedSearchMetrics) {
942        if k == 0 || tree.len == 0 {
943            return (Vec::new(), BoundedSearchMetrics::default());
944        }
945        let capacity = k.min(tree.len);
946        let mut ranks = NearestBound::new(k, capacity);
947        let mut frontier: SearchFrontier<FrontierNode<'_, T>> = SearchFrontier::new();
948        let mut metrics = BoundedSearchMetrics::default();
949        frontier.push(FrontierNode {
950            dist: 0.0,
951            node: &tree.root,
952        });
953        while let Some(FrontierNode { dist, node }) = frontier.pop() {
954            if dist.total_cmp(&ranks.bound()).is_ge() {
955                metrics.terminated_by_bound += 1;
956                break;
957            }
958            match node {
959                Node::Leaf(leaf) => {
960                    let values = leaf.values();
961                    metrics.leaf_expansions += 1;
962                    let reverse = nearer_y_end_first
963                        && values
964                            .first()
965                            .zip(values.last())
966                            .is_some_and(|(first, last)| {
967                                let first_y = first.bounds().center()[1];
968                                let last_y = last.bounds().center()[1];
969                                (last_y - query[1]).abs() < (first_y - query[1]).abs()
970                            });
971                    metrics.reversed_leaf_scans += usize::from(reverse);
972                    if leaf_bvh_terminal_size != 0 {
973                        record_leaf_bvh(
974                            values,
975                            leaf_bvh_terminal_size,
976                            query,
977                            &mut ranks,
978                            &mut metrics,
979                        );
980                    } else if center_out && leaf_group_size != 0 {
981                        record_center_out_leaf_groups(
982                            values,
983                            leaf_group_size,
984                            query,
985                            &mut ranks,
986                            &mut metrics,
987                        );
988                    } else if center_out {
989                        metrics.value_distance_evaluations += values.len();
990                        record_center_out_leaf(values, query, &mut ranks, &mut metrics);
991                    } else if leaf_group_size != 0 {
992                        // Model precomputed bounds over contiguous STR-y groups.
993                        // Computing the bounds here is test-only instrumentation;
994                        // the counters describe query work if they were stored.
995                        if reverse {
996                            for group in values.chunks(leaf_group_size).rev() {
997                                record_leaf_group(group, true, query, &mut ranks, &mut metrics);
998                            }
999                        } else {
1000                            for group in values.chunks(leaf_group_size) {
1001                                record_leaf_group(group, false, query, &mut ranks, &mut metrics);
1002                            }
1003                        }
1004                    } else if reverse {
1005                        metrics.value_distance_evaluations += values.len();
1006                        for value in values.iter().rev() {
1007                            record_value_candidate(value, query, &mut ranks, &mut metrics);
1008                        }
1009                    } else {
1010                        metrics.value_distance_evaluations += values.len();
1011                        for value in values {
1012                            record_value_candidate(value, query, &mut ranks, &mut metrics);
1013                        }
1014                    }
1015                }
1016                Node::Branch(children) => {
1017                    metrics.branch_expansions += 1;
1018                    metrics.child_distance_evaluations += children.len();
1019                    for (bounds, child) in children {
1020                        let dist = bounds.comparable_min_distance_to(query);
1021                        if dist.total_cmp(&ranks.bound()).is_lt() {
1022                            metrics.child_pushes += 1;
1023                            frontier.push(FrontierNode { dist, node: child });
1024                        } else {
1025                            metrics.child_pruned += 1;
1026                        }
1027                    }
1028                }
1029            }
1030        }
1031        let frontier_metrics = frontier.metrics();
1032        metrics.frontier_pushes = frontier_metrics.pushes;
1033        metrics.frontier_pops = frontier_metrics.pops;
1034        metrics.frontier_high_water = frontier_metrics.high_water;
1035        metrics.rank = ranks.metrics();
1036        (ranks.into_values(), metrics)
1037    }
1038
1039    fn nearest_distance_ordered_groups_with_metrics<T: Indexable, Params: SplitParameters>(
1040        tree: &Rtree<T, Params>,
1041        query: [f64; 2],
1042        k: usize,
1043        group_size: usize,
1044    ) -> (Vec<&T>, BoundedSearchMetrics) {
1045        if k == 0 || tree.len == 0 {
1046            return (Vec::new(), BoundedSearchMetrics::default());
1047        }
1048        let mut ranks = NearestBound::new(k, k.min(tree.len));
1049        let mut frontier: SearchFrontier<FrontierNode<'_, T>> = SearchFrontier::new();
1050        let mut metrics = BoundedSearchMetrics::default();
1051        frontier.push(FrontierNode {
1052            dist: 0.0,
1053            node: &tree.root,
1054        });
1055        while let Some(FrontierNode { dist, node }) = frontier.pop() {
1056            if dist.total_cmp(&ranks.bound()).is_ge() {
1057                metrics.terminated_by_bound += 1;
1058                break;
1059            }
1060            match node {
1061                Node::Leaf(leaf) => {
1062                    metrics.leaf_expansions += 1;
1063                    record_distance_ordered_leaf_groups(
1064                        leaf.values(),
1065                        group_size,
1066                        query,
1067                        &mut ranks,
1068                        &mut metrics,
1069                    );
1070                }
1071                Node::Branch(children) => {
1072                    metrics.branch_expansions += 1;
1073                    metrics.child_distance_evaluations += children.len();
1074                    for (bounds, child) in children {
1075                        let dist = bounds.comparable_min_distance_to(query);
1076                        if dist.total_cmp(&ranks.bound()).is_lt() {
1077                            metrics.child_pushes += 1;
1078                            frontier.push(FrontierNode { dist, node: child });
1079                        } else {
1080                            metrics.child_pruned += 1;
1081                        }
1082                    }
1083                }
1084            }
1085        }
1086        let frontier_metrics = frontier.metrics();
1087        metrics.frontier_pushes = frontier_metrics.pushes;
1088        metrics.frontier_pops = frontier_metrics.pops;
1089        metrics.frontier_high_water = frontier_metrics.high_water;
1090        metrics.rank = ranks.metrics();
1091        (ranks.into_values(), metrics)
1092    }
1093
1094    fn record_distance_ordered_leaf_groups<'a, T: Indexable>(
1095        values: &'a [T],
1096        group_size: usize,
1097        query: [f64; 2],
1098        ranks: &mut NearestBound<&'a T>,
1099        metrics: &mut BoundedSearchMetrics,
1100    ) {
1101        let mut ordered: Vec<(f64, usize)> = values
1102            .chunks(group_size)
1103            .enumerate()
1104            .map(|(index, group)| {
1105                let bounds = group
1106                    .iter()
1107                    .map(Indexable::bounds)
1108                    .reduce(|a, b| a.union(&b))
1109                    .expect("chunks are non-empty");
1110                metrics.leaf_group_bound_evaluations += 1;
1111                (bounds.comparable_min_distance_to(query), index)
1112            })
1113            .collect();
1114        ordered.sort_unstable_by(|a, b| {
1115            metrics.leaf_group_order_comparisons += 1;
1116            a.0.total_cmp(&b.0)
1117        });
1118        for (group_dist, group_index) in ordered {
1119            if group_dist.total_cmp(&ranks.bound()).is_ge() {
1120                metrics.leaf_groups_pruned += 1;
1121                continue;
1122            }
1123            metrics.leaf_groups_scanned += 1;
1124            let start = group_index * group_size;
1125            let group = &values[start..(start + group_size).min(values.len())];
1126            metrics.value_distance_evaluations += group.len();
1127            let reverse = group
1128                .first()
1129                .zip(group.last())
1130                .is_some_and(|(first, last)| {
1131                    let first_y = first.bounds().center()[1];
1132                    let last_y = last.bounds().center()[1];
1133                    (last_y - query[1]).abs() < (first_y - query[1]).abs()
1134                });
1135            if reverse {
1136                for value in group.iter().rev() {
1137                    record_value_candidate(value, query, ranks, metrics);
1138                }
1139            } else {
1140                for value in group {
1141                    record_value_candidate(value, query, ranks, metrics);
1142                }
1143            }
1144        }
1145    }
1146
1147    fn nearest_depth_first_with_metrics<T: Indexable, Params: SplitParameters>(
1148        tree: &Rtree<T, Params>,
1149        query: [f64; 2],
1150        k: usize,
1151    ) -> (Vec<&T>, BoundedSearchMetrics) {
1152        if k == 0 || tree.len == 0 {
1153            return (Vec::new(), BoundedSearchMetrics::default());
1154        }
1155        let mut ranks = NearestBound::new(k, k.min(tree.len));
1156        let mut metrics = BoundedSearchMetrics::default();
1157        record_depth_first_node(&tree.root, query, true, &mut ranks, &mut metrics);
1158        metrics.rank = ranks.metrics();
1159        (ranks.into_values(), metrics)
1160    }
1161
1162    fn record_depth_first_node<'a, T: Indexable>(
1163        node: &'a Node<T>,
1164        query: [f64; 2],
1165        nearer_y_end_first: bool,
1166        ranks: &mut NearestBound<&'a T>,
1167        metrics: &mut BoundedSearchMetrics,
1168    ) {
1169        match node {
1170            Node::Leaf(leaf) => {
1171                let values = leaf.values();
1172                metrics.leaf_expansions += 1;
1173                metrics.value_distance_evaluations += values.len();
1174                let reverse = nearer_y_end_first
1175                    && values
1176                        .first()
1177                        .zip(values.last())
1178                        .is_some_and(|(first, last)| {
1179                            let first_y = first.bounds().center()[1];
1180                            let last_y = last.bounds().center()[1];
1181                            (last_y - query[1]).abs() < (first_y - query[1]).abs()
1182                        });
1183                metrics.reversed_leaf_scans += usize::from(reverse);
1184                if reverse {
1185                    for value in values.iter().rev() {
1186                        record_value_candidate(value, query, ranks, metrics);
1187                    }
1188                } else {
1189                    for value in values {
1190                        record_value_candidate(value, query, ranks, metrics);
1191                    }
1192                }
1193            }
1194            Node::Branch(children) => {
1195                metrics.branch_expansions += 1;
1196                metrics.child_distance_evaluations += children.len();
1197                let mut ordered: Vec<FrontierNode<'_, T>> = children
1198                    .iter()
1199                    .map(|(bounds, child)| FrontierNode {
1200                        dist: bounds.comparable_min_distance_to(query),
1201                        node: child,
1202                    })
1203                    .collect();
1204                ordered.sort_unstable_by(|a, b| {
1205                    metrics.child_order_comparisons += 1;
1206                    a.dist.total_cmp(&b.dist)
1207                });
1208                for (index, FrontierNode { dist, node }) in ordered.iter().enumerate() {
1209                    if dist.total_cmp(&ranks.bound()).is_ge() {
1210                        metrics.child_pruned += ordered.len() - index;
1211                        break;
1212                    }
1213                    metrics.child_pushes += 1;
1214                    record_depth_first_node(node, query, nearer_y_end_first, ranks, metrics);
1215                }
1216            }
1217        }
1218    }
1219
1220    fn record_center_out_leaf_groups<'a, T: Indexable>(
1221        values: &'a [T],
1222        group_size: usize,
1223        query: [f64; 2],
1224        ranks: &mut NearestBound<&'a T>,
1225        metrics: &mut BoundedSearchMetrics,
1226    ) {
1227        let group_bounds: Vec<Bounds> = values
1228            .chunks(group_size)
1229            .map(|group| {
1230                group
1231                    .iter()
1232                    .map(Indexable::bounds)
1233                    .reduce(|a, b| a.union(&b))
1234                    .expect("chunks are non-empty")
1235            })
1236            .collect();
1237        let mut upper = group_bounds.partition_point(|bounds| bounds.center()[1] < query[1]);
1238        let mut lower = upper;
1239        while lower != 0 || upper != group_bounds.len() {
1240            let take_lower = if lower == 0 {
1241                false
1242            } else if upper == group_bounds.len() {
1243                true
1244            } else {
1245                let lower_y = group_bounds[lower - 1].center()[1];
1246                let upper_y = group_bounds[upper].center()[1];
1247                (query[1] - lower_y).abs() <= (upper_y - query[1]).abs()
1248            };
1249            let group_index = if take_lower {
1250                lower -= 1;
1251                lower
1252            } else {
1253                let group_index = upper;
1254                upper += 1;
1255                group_index
1256            };
1257            metrics.leaf_group_bound_evaluations += 1;
1258            let group_dist = group_bounds[group_index].comparable_min_distance_to(query);
1259            if group_dist.total_cmp(&ranks.bound()).is_ge() {
1260                metrics.leaf_groups_pruned += 1;
1261                continue;
1262            }
1263            metrics.leaf_groups_scanned += 1;
1264            let start = group_index * group_size;
1265            let end = (start + group_size).min(values.len());
1266            let group = &values[start..end];
1267            metrics.value_distance_evaluations += group.len();
1268            record_center_out_leaf(group, query, ranks, metrics);
1269        }
1270    }
1271
1272    fn record_center_out_leaf<'a, T: Indexable>(
1273        values: &'a [T],
1274        query: [f64; 2],
1275        ranks: &mut NearestBound<&'a T>,
1276        metrics: &mut BoundedSearchMetrics,
1277    ) {
1278        let mut upper = values.partition_point(|value| value.bounds().center()[1] < query[1]);
1279        let mut lower = upper;
1280        while lower != 0 || upper != values.len() {
1281            let take_lower = if lower == 0 {
1282                false
1283            } else if upper == values.len() {
1284                true
1285            } else {
1286                let lower_y = values[lower - 1].bounds().center()[1];
1287                let upper_y = values[upper].bounds().center()[1];
1288                (query[1] - lower_y).abs() <= (upper_y - query[1]).abs()
1289            };
1290            let value = if take_lower {
1291                lower -= 1;
1292                &values[lower]
1293            } else {
1294                let value = &values[upper];
1295                upper += 1;
1296                value
1297            };
1298            record_value_candidate(value, query, ranks, metrics);
1299        }
1300    }
1301
1302    fn record_leaf_bvh<'a, T: Indexable>(
1303        values: &'a [T],
1304        terminal_size: usize,
1305        query: [f64; 2],
1306        ranks: &mut NearestBound<&'a T>,
1307        metrics: &mut BoundedSearchMetrics,
1308    ) {
1309        if values.len() <= terminal_size {
1310            metrics.leaf_groups_scanned += 1;
1311            metrics.value_distance_evaluations += values.len();
1312            let reverse = values
1313                .first()
1314                .zip(values.last())
1315                .is_some_and(|(first, last)| {
1316                    let first_y = first.bounds().center()[1];
1317                    let last_y = last.bounds().center()[1];
1318                    (last_y - query[1]).abs() < (first_y - query[1]).abs()
1319                });
1320            if reverse {
1321                for value in values.iter().rev() {
1322                    record_value_candidate(value, query, ranks, metrics);
1323                }
1324            } else {
1325                for value in values {
1326                    record_value_candidate(value, query, ranks, metrics);
1327                }
1328            }
1329            return;
1330        }
1331
1332        let middle = values.len() / 2;
1333        let (lower, upper) = values.split_at(middle);
1334        let child_bounds = [lower, upper].map(|child| {
1335            child
1336                .iter()
1337                .map(Indexable::bounds)
1338                .reduce(|a, b| a.union(&b))
1339                .expect("BVH children are non-empty")
1340        });
1341        metrics.leaf_group_bound_evaluations += 2;
1342        metrics.leaf_group_order_comparisons += 1;
1343        let distances = child_bounds.map(|bounds| bounds.comparable_min_distance_to(query));
1344        let order = if distances[0].total_cmp(&distances[1]).is_le() {
1345            [0, 1]
1346        } else {
1347            [1, 0]
1348        };
1349        let children = [lower, upper];
1350        for index in order {
1351            if distances[index].total_cmp(&ranks.bound()).is_lt() {
1352                record_leaf_bvh(children[index], terminal_size, query, ranks, metrics);
1353            } else {
1354                metrics.leaf_groups_pruned += 1;
1355            }
1356        }
1357    }
1358
1359    fn record_leaf_group<'a, T: Indexable>(
1360        group: &'a [T],
1361        reverse: bool,
1362        query: [f64; 2],
1363        ranks: &mut NearestBound<&'a T>,
1364        metrics: &mut BoundedSearchMetrics,
1365    ) {
1366        metrics.leaf_group_bound_evaluations += 1;
1367        let bounds = group
1368            .iter()
1369            .map(Indexable::bounds)
1370            .reduce(|a, b| a.union(&b))
1371            .expect("chunks are non-empty");
1372        let group_dist = bounds.comparable_min_distance_to(query);
1373        if group_dist.total_cmp(&ranks.bound()).is_ge() {
1374            metrics.leaf_groups_pruned += 1;
1375            return;
1376        }
1377        metrics.leaf_groups_scanned += 1;
1378        metrics.value_distance_evaluations += group.len();
1379        if reverse {
1380            for value in group.iter().rev() {
1381                record_value_candidate(value, query, ranks, metrics);
1382            }
1383        } else {
1384            for value in group {
1385                record_value_candidate(value, query, ranks, metrics);
1386            }
1387        }
1388    }
1389
1390    fn record_value_candidate<'a, T: Indexable>(
1391        value: &'a T,
1392        query: [f64; 2],
1393        ranks: &mut NearestBound<&'a T>,
1394        metrics: &mut BoundedSearchMetrics,
1395    ) {
1396        let dist = value.bounds().comparable_min_distance_to(query);
1397        if dist.total_cmp(&ranks.bound()).is_lt() {
1398            metrics.value_bound_passes += 1;
1399            ranks.admit_better(dist, value);
1400        } else {
1401            metrics.value_bound_rejections += 1;
1402        }
1403    }
1404
1405    #[test]
1406    fn empty_tree() {
1407        let t: Rtree<P> = Rtree::new();
1408        assert!(t.is_empty());
1409        assert_eq!(t.len(), 0);
1410    }
1411
1412    #[test]
1413    fn private_metric_helpers_handle_empty_queries_and_small_integer_roots() {
1414        assert_eq!(isqrt_ceil(0), 0);
1415        assert_eq!(isqrt_ceil(1), 1);
1416        assert_eq!(isqrt_ceil(2), 2);
1417        assert_eq!(isqrt_ceil(4), 2);
1418
1419        let ordered_values: Vec<P> = (0..12).map(|x| P::new(f64::from(x), 0.0)).collect();
1420        let mut ranks = NearestBound::new(1, 1);
1421        let mut metrics = BoundedSearchMetrics::default();
1422        record_distance_ordered_leaf_groups(
1423            &ordered_values,
1424            2,
1425            [0.0, 0.0],
1426            &mut ranks,
1427            &mut metrics,
1428        );
1429        assert!(metrics.leaf_group_order_comparisons > 0);
1430        assert!(metrics.leaf_groups_pruned > 0);
1431
1432        let tree = Rtree::<P>::new();
1433        assert!(
1434            nearest_with_metrics(&tree, [0.0, 0.0], 0, false, 8, false, 8)
1435                .0
1436                .is_empty()
1437        );
1438        assert!(
1439            nearest_distance_ordered_groups_with_metrics(&tree, [0.0, 0.0], 0, 8)
1440                .0
1441                .is_empty()
1442        );
1443        assert!(
1444            nearest_depth_first_with_metrics(&tree, [0.0, 0.0], 0)
1445                .0
1446                .is_empty()
1447        );
1448    }
1449
1450    /// `Default` builds the same empty tree as `new()`.
1451    #[test]
1452    fn default_tree_is_empty() {
1453        let t: Rtree<P> = Rtree::default();
1454        assert!(t.is_empty());
1455        assert_eq!(t.len(), 0);
1456    }
1457
1458    /// `FrontierNode` equality is keyed on the distance (total order),
1459    /// not on the node identity.
1460    #[test]
1461    fn frontier_node_eq_compares_distance() {
1462        let mut t: Rtree<P> = Rtree::new();
1463        t.insert(P::new(0.0, 0.0));
1464        let a = FrontierNode {
1465            dist: 1.5,
1466            node: &t.root,
1467        };
1468        let b = FrontierNode {
1469            dist: 1.5,
1470            node: &t.root,
1471        };
1472        let c = FrontierNode {
1473            dist: 2.5,
1474            node: &t.root,
1475        };
1476        assert!(a == b);
1477        assert!(a != c);
1478    }
1479
1480    #[test]
1481    fn insert_many_points_keeps_len() {
1482        let mut t: Rtree<P> = Rtree::new();
1483        for i in 0..1000 {
1484            let x = f64::from(i % 100);
1485            let y = f64::from(i / 100);
1486            t.insert(P::new(x, y));
1487        }
1488        assert_eq!(t.len(), 1000);
1489        assert!(
1490            t.height() >= 2,
1491            "1000 points should build a multi-level tree"
1492        );
1493    }
1494
1495    #[test]
1496    fn query_intersects_finds_the_point() {
1497        let mut t: Rtree<P> = Rtree::new();
1498        for i in 0..200 {
1499            t.insert(P::new(f64::from(i), 0.0));
1500        }
1501        let hits = t.query(Predicate::Intersects(Bounds::new([9.5, -1.0], [10.5, 1.0])));
1502        assert_eq!(hits.len(), 1);
1503    }
1504
1505    #[test]
1506    fn query_within_a_window() {
1507        let mut t: Rtree<P> = Rtree::new();
1508        for x in 0..10 {
1509            for y in 0..10 {
1510                t.insert(P::new(f64::from(x), f64::from(y)));
1511            }
1512        }
1513        // The window [2,5]×[2,5] contains a 4×4 block of points.
1514        let hits = t.query(Predicate::CoveredBy(Bounds::new([2.0, 2.0], [5.0, 5.0])));
1515        assert_eq!(hits.len(), 16);
1516    }
1517
1518    #[test]
1519    fn nearest_returns_closest_first() {
1520        let mut t: Rtree<P> = Rtree::new();
1521        for i in 0..100 {
1522            t.insert(P::new(f64::from(i), 0.0));
1523        }
1524        let near = t.nearest([10.2, 0.0], 3);
1525        assert_eq!(near.len(), 3);
1526        // The three closest to x=10.2 are x=10, 11, 9 in some order.
1527        let mut xs: Vec<f64> = near.iter().map(|p| p.get::<0>()).collect();
1528        xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
1529        assert_eq!(xs, [9.0, 10.0, 11.0]);
1530    }
1531
1532    #[test]
1533    fn linear_split_also_works() {
1534        let mut t: Rtree<P, Linear<8, 3>> = Rtree::new();
1535        for i in 0..500 {
1536            t.insert(P::new(f64::from(i % 25), f64::from(i / 25)));
1537        }
1538        assert_eq!(t.len(), 500);
1539        let hits = t.query(Predicate::Intersects(Bounds::new([0.0, 0.0], [3.0, 3.0])));
1540        assert!(!hits.is_empty());
1541    }
1542
1543    fn uniform_points(n: usize) -> Vec<P> {
1544        let mut lcg = Lcg::new();
1545        (0..n)
1546            .map(|_| {
1547                let x = lcg.next_f64() * 50_000.0;
1548                let y = lcg.next_f64() * 50_000.0;
1549                P::new(x, y)
1550            })
1551            .collect()
1552    }
1553
1554    fn clustered_points(n: usize) -> Vec<P> {
1555        const CLUSTER_COUNT: usize = 16;
1556        const CLUSTER_RADIUS: f64 = 100.0;
1557        const FIELD: f64 = 50_000.0;
1558
1559        let mut lcg = Lcg::new();
1560        let centers: Vec<[f64; 2]> = (0..CLUSTER_COUNT)
1561            .map(|_| [lcg.next_f64() * FIELD, lcg.next_f64() * FIELD])
1562            .collect();
1563        (0..n)
1564            .map(|i| {
1565                let center = centers[i % CLUSTER_COUNT];
1566                P::new(
1567                    center[0] + lcg.next_f64() * 2.0 * CLUSTER_RADIUS - CLUSTER_RADIUS,
1568                    center[1] + lcg.next_f64() * 2.0 * CLUSTER_RADIUS - CLUSTER_RADIUS,
1569                )
1570            })
1571            .collect()
1572    }
1573
1574    fn profile_queries(q: usize) -> Vec<[f64; 2]> {
1575        let mut lcg = Lcg::new();
1576        (0..q)
1577            .map(|_| {
1578                let x = lcg.next_f64() * 50_000.0;
1579                lcg.next_f64();
1580                let y = lcg.next_f64() * 50_000.0;
1581                [x, y]
1582            })
1583            .collect()
1584    }
1585
1586    fn report_bounded_metrics(
1587        construction: &str,
1588        distribution: &str,
1589        leaf_order: &str,
1590        expected_results: usize,
1591        total: &BoundedSearchMetrics,
1592    ) {
1593        eprintln!(
1594            "[rtree-bounded-shape] construction={construction} distribution={distribution} leaf_order={leaf_order} expected_results={expected_results} frontier_pushes={} frontier_pops={} frontier_high_water={} terminated_by_bound={} branch_expansions={} child_distance_evaluations={} child_order_comparisons={} child_pushes={} child_pruned={} leaf_expansions={} reversed_leaf_scans={} leaf_group_bound_evaluations={} leaf_group_order_comparisons={} leaf_groups_scanned={} leaf_groups_pruned={} value_distance_evaluations={} value_bound_passes={} value_bound_rejections={} rank_calls={} rank_partition_comparisons={} rank_admissions={} rank_replacements={} rank_shifted_ranks={}",
1595            total.frontier_pushes,
1596            total.frontier_pops,
1597            total.frontier_high_water,
1598            total.terminated_by_bound,
1599            total.branch_expansions,
1600            total.child_distance_evaluations,
1601            total.child_order_comparisons,
1602            total.child_pushes,
1603            total.child_pruned,
1604            total.leaf_expansions,
1605            total.reversed_leaf_scans,
1606            total.leaf_group_bound_evaluations,
1607            total.leaf_group_order_comparisons,
1608            total.leaf_groups_scanned,
1609            total.leaf_groups_pruned,
1610            total.value_distance_evaluations,
1611            total.value_bound_passes,
1612            total.value_bound_rejections,
1613            total.rank.calls,
1614            total.rank.partition_comparisons,
1615            total.rank.admissions,
1616            total.rank.replacements,
1617            total.rank.shifted_ranks,
1618        );
1619    }
1620
1621    #[test]
1622    fn records_bounded_search_shape() {
1623        const N: usize = 50_000;
1624        const Q: usize = 100;
1625        const K: usize = 8;
1626
1627        for (construction, distribution, points) in [
1628            ("bulk", "uniform", uniform_points(N)),
1629            ("bulk", "clustered", clustered_points(N)),
1630            ("inserted", "uniform", uniform_points(N)),
1631            ("inserted", "clustered", clustered_points(N)),
1632        ] {
1633            let tree: Rtree<P> = if construction == "bulk" {
1634                points.into_iter().collect()
1635            } else {
1636                let mut tree = Rtree::new();
1637                for point in points {
1638                    tree.insert(point);
1639                }
1640                tree
1641            };
1642            let mut forward_total = BoundedSearchMetrics::default();
1643            let mut nearer_y_total = BoundedSearchMetrics::default();
1644            for query in profile_queries(Q) {
1645                let expected = tree.nearest(query, K);
1646                let (forward, metrics) = nearest_with_metrics(&tree, query, K, false, 0, false, 0);
1647                assert_eq!(forward, expected);
1648                forward_total.add(&metrics);
1649                let (nearer_y, metrics) = nearest_with_metrics(&tree, query, K, true, 0, false, 0);
1650                assert_eq!(nearer_y, expected);
1651                nearer_y_total.add(&metrics);
1652            }
1653            report_bounded_metrics(construction, distribution, "forward", Q * K, &forward_total);
1654            report_bounded_metrics(
1655                construction,
1656                distribution,
1657                "nearer-y-end",
1658                Q * K,
1659                &nearer_y_total,
1660            );
1661        }
1662    }
1663
1664    fn record_inserted_parameter_shape<Params: SplitParameters>(
1665        parameters: &str,
1666        distribution: &str,
1667        points: &[P],
1668    ) {
1669        const Q: usize = 100;
1670        const K: usize = 8;
1671
1672        let tree = insert_built::<Params>(points);
1673        let mut total = BoundedSearchMetrics::default();
1674        for query in profile_queries(Q) {
1675            let (_, metrics) = nearest_with_metrics(&tree, query, K, false, 0, false, 0);
1676            total.add(&metrics);
1677        }
1678        report_bounded_metrics(parameters, distribution, "forward", Q * K, &total);
1679    }
1680
1681    fn record_bulk_parameter_shape<Params: SplitParameters>(
1682        parameters: &str,
1683        distribution: &str,
1684        points: &[P],
1685    ) {
1686        const Q: usize = 100;
1687        const K: usize = 8;
1688
1689        let tree: Rtree<P, Params> = points.iter().copied().collect();
1690        let mut total = BoundedSearchMetrics::default();
1691        for query in profile_queries(Q) {
1692            let expected = tree.nearest(query, K);
1693            let (observed, metrics) = nearest_with_metrics(&tree, query, K, true, 0, false, 0);
1694            assert_eq!(observed, expected);
1695            total.add(&metrics);
1696        }
1697        report_bounded_metrics(parameters, distribution, "nearer-y-end", Q * K, &total);
1698    }
1699
1700    fn record_bulk_group_shape(group_size: usize, distribution: &str, points: &[P]) {
1701        const Q: usize = 100;
1702        const K: usize = 8;
1703
1704        let tree: Rtree<P> = points.iter().copied().collect();
1705        let mut total = BoundedSearchMetrics::default();
1706        for query in profile_queries(Q) {
1707            let expected = tree.nearest(query, K);
1708            let (observed, metrics) =
1709                nearest_with_metrics(&tree, query, K, true, group_size, false, 0);
1710            assert_eq!(observed, expected);
1711            total.add(&metrics);
1712        }
1713        report_bounded_metrics(
1714            &alloc::format!("bulk-group{group_size}"),
1715            distribution,
1716            "nearer-y-end",
1717            Q * K,
1718            &total,
1719        );
1720    }
1721
1722    fn record_bulk_center_out_shape(distribution: &str, points: &[P]) {
1723        const Q: usize = 100;
1724        const K: usize = 8;
1725
1726        let tree: Rtree<P> = points.iter().copied().collect();
1727        let mut total = BoundedSearchMetrics::default();
1728        for query in profile_queries(Q) {
1729            let expected = tree.nearest(query, K);
1730            let (observed, metrics) = nearest_with_metrics(&tree, query, K, false, 0, true, 0);
1731            assert_eq!(observed, expected);
1732            total.add(&metrics);
1733        }
1734        report_bounded_metrics("bulk-center-out", distribution, "center-out", Q * K, &total);
1735    }
1736
1737    fn record_bulk_center_out_group_shape(group_size: usize, distribution: &str, points: &[P]) {
1738        const Q: usize = 100;
1739        const K: usize = 8;
1740
1741        let tree: Rtree<P> = points.iter().copied().collect();
1742        let mut total = BoundedSearchMetrics::default();
1743        for query in profile_queries(Q) {
1744            let expected = tree.nearest(query, K);
1745            let (observed, metrics) =
1746                nearest_with_metrics(&tree, query, K, false, group_size, true, 0);
1747            assert_eq!(observed, expected);
1748            total.add(&metrics);
1749        }
1750        report_bounded_metrics(
1751            &alloc::format!("bulk-center-group{group_size}"),
1752            distribution,
1753            "center-out-groups",
1754            Q * K,
1755            &total,
1756        );
1757    }
1758
1759    fn record_bulk_depth_first_shape(distribution: &str, points: &[P]) {
1760        const Q: usize = 100;
1761        const K: usize = 8;
1762
1763        let tree: Rtree<P> = points.iter().copied().collect();
1764        let mut total = BoundedSearchMetrics::default();
1765        for query in profile_queries(Q) {
1766            let expected = tree.nearest(query, K);
1767            let (observed, metrics) = nearest_depth_first_with_metrics(&tree, query, K);
1768            assert_eq!(observed, expected);
1769            total.add(&metrics);
1770        }
1771        report_bounded_metrics(
1772            "bulk-depth-first",
1773            distribution,
1774            "nearer-y-end",
1775            Q * K,
1776            &total,
1777        );
1778    }
1779
1780    fn record_bulk_distance_group_shape(group_size: usize, distribution: &str, points: &[P]) {
1781        const Q: usize = 100;
1782        const K: usize = 8;
1783
1784        let tree: Rtree<P> = points.iter().copied().collect();
1785        let mut total = BoundedSearchMetrics::default();
1786        for query in profile_queries(Q) {
1787            let expected = tree.nearest(query, K);
1788            let (observed, metrics) =
1789                nearest_distance_ordered_groups_with_metrics(&tree, query, K, group_size);
1790            assert_eq!(observed, expected);
1791            total.add(&metrics);
1792        }
1793        report_bounded_metrics(
1794            &alloc::format!("bulk-distance-group{group_size}"),
1795            distribution,
1796            "distance-ordered-groups",
1797            Q * K,
1798            &total,
1799        );
1800    }
1801
1802    fn record_bulk_leaf_bvh_shape(terminal_size: usize, distribution: &str, points: &[P]) {
1803        const Q: usize = 100;
1804        const K: usize = 8;
1805
1806        let tree: Rtree<P> = points.iter().copied().collect();
1807        let mut total = BoundedSearchMetrics::default();
1808        for query in profile_queries(Q) {
1809            let expected = tree.nearest(query, K);
1810            let (observed, metrics) =
1811                nearest_with_metrics(&tree, query, K, false, 0, false, terminal_size);
1812            assert_eq!(observed, expected);
1813            total.add(&metrics);
1814        }
1815        report_bounded_metrics(
1816            &alloc::format!("bulk-leaf-bvh{terminal_size}"),
1817            distribution,
1818            "distance-ordered-bvh",
1819            Q * K,
1820            &total,
1821        );
1822    }
1823
1824    #[test]
1825    fn records_bulk_leaf_bvh_shape() {
1826        const N: usize = 50_000;
1827
1828        for (distribution, points) in [
1829            ("uniform", uniform_points(N)),
1830            ("clustered", clustered_points(N)),
1831        ] {
1832            for terminal_size in [2, 4, 8] {
1833                record_bulk_leaf_bvh_shape(terminal_size, distribution, &points);
1834            }
1835        }
1836    }
1837
1838    #[test]
1839    fn records_bulk_bounded_distance_group_shape() {
1840        const N: usize = 50_000;
1841
1842        for (distribution, points) in [
1843            ("uniform", uniform_points(N)),
1844            ("clustered", clustered_points(N)),
1845        ] {
1846            for group_size in [4, 8] {
1847                record_bulk_distance_group_shape(group_size, distribution, &points);
1848            }
1849        }
1850    }
1851
1852    #[test]
1853    fn records_bulk_bounded_depth_first_shape() {
1854        const N: usize = 50_000;
1855
1856        for (distribution, points) in [
1857            ("uniform", uniform_points(N)),
1858            ("clustered", clustered_points(N)),
1859        ] {
1860            record_bulk_depth_first_shape(distribution, &points);
1861        }
1862    }
1863
1864    #[test]
1865    fn records_bulk_bounded_center_out_group_shape() {
1866        const N: usize = 50_000;
1867
1868        for (distribution, points) in [
1869            ("uniform", uniform_points(N)),
1870            ("clustered", clustered_points(N)),
1871        ] {
1872            for group_size in [2, 4, 8] {
1873                record_bulk_center_out_group_shape(group_size, distribution, &points);
1874            }
1875        }
1876    }
1877
1878    #[test]
1879    fn records_bulk_bounded_center_out_shape() {
1880        const N: usize = 50_000;
1881
1882        for (distribution, points) in [
1883            ("uniform", uniform_points(N)),
1884            ("clustered", clustered_points(N)),
1885        ] {
1886            record_bulk_center_out_shape(distribution, &points);
1887        }
1888    }
1889
1890    #[test]
1891    fn records_bulk_bounded_group_shape() {
1892        const N: usize = 50_000;
1893
1894        for (distribution, points) in [
1895            ("uniform", uniform_points(N)),
1896            ("clustered", clustered_points(N)),
1897        ] {
1898            for group_size in [2, 4, 8, 16] {
1899                record_bulk_group_shape(group_size, distribution, &points);
1900            }
1901        }
1902    }
1903
1904    #[test]
1905    fn records_bulk_bounded_parameter_shape() {
1906        const N: usize = 50_000;
1907
1908        for (distribution, points) in [
1909            ("uniform", uniform_points(N)),
1910            ("clustered", clustered_points(N)),
1911        ] {
1912            record_bulk_parameter_shape::<AsymmetricRStarSplit<4, 2, 4, 2>>(
1913                "bulk-b4-l4",
1914                distribution,
1915                &points,
1916            );
1917            record_bulk_parameter_shape::<AsymmetricRStarSplit<6, 2, 4, 2>>(
1918                "bulk-b6-l4",
1919                distribution,
1920                &points,
1921            );
1922            record_bulk_parameter_shape::<AsymmetricRStarSplit<8, 3, 4, 2>>(
1923                "bulk-b8-l4",
1924                distribution,
1925                &points,
1926            );
1927            record_bulk_parameter_shape::<AsymmetricRStarSplit<12, 4, 4, 2>>(
1928                "bulk-b12-l4",
1929                distribution,
1930                &points,
1931            );
1932            record_bulk_parameter_shape::<AsymmetricRStarSplit<6, 2, 6, 2>>(
1933                "bulk-b6-l6",
1934                distribution,
1935                &points,
1936            );
1937            record_bulk_parameter_shape::<AsymmetricRStarSplit<4, 2, 8, 3>>(
1938                "bulk-b4-l8",
1939                distribution,
1940                &points,
1941            );
1942            record_bulk_parameter_shape::<AsymmetricRStarSplit<6, 2, 8, 3>>(
1943                "bulk-b6-l8",
1944                distribution,
1945                &points,
1946            );
1947            record_bulk_parameter_shape::<AsymmetricRStarSplit<8, 3, 8, 3>>(
1948                "bulk-b8-l8",
1949                distribution,
1950                &points,
1951            );
1952            record_bulk_parameter_shape::<AsymmetricRStarSplit<8, 3, 12, 4>>(
1953                "bulk-b8-l12",
1954                distribution,
1955                &points,
1956            );
1957            record_bulk_parameter_shape::<AsymmetricRStarSplit<8, 3, 16, 4>>(
1958                "bulk-b8-l16",
1959                distribution,
1960                &points,
1961            );
1962            record_bulk_parameter_shape::<AsymmetricRStarSplit<8, 3, 24, 7>>(
1963                "bulk-b8-l24",
1964                distribution,
1965                &points,
1966            );
1967            record_bulk_parameter_shape::<AsymmetricRStarSplit<8, 3, 32, 9>>(
1968                "bulk-b8-l32",
1969                distribution,
1970                &points,
1971            );
1972            record_bulk_parameter_shape::<AsymmetricRStarSplit<12, 4, 8, 3>>(
1973                "bulk-b12-l8",
1974                distribution,
1975                &points,
1976            );
1977            record_bulk_parameter_shape::<AsymmetricRStarSplit<16, 4, 8, 3>>(
1978                "bulk-b16-l8",
1979                distribution,
1980                &points,
1981            );
1982            record_bulk_parameter_shape::<AsymmetricRStarSplit<32, 9, 8, 3>>(
1983                "bulk-b32-l8",
1984                distribution,
1985                &points,
1986            );
1987            record_bulk_parameter_shape::<AsymmetricRStarSplit<12, 4, 16, 4>>(
1988                "bulk-b12-l16",
1989                distribution,
1990                &points,
1991            );
1992            record_bulk_parameter_shape::<AsymmetricRStarSplit<16, 4, 16, 4>>(
1993                "bulk-b16-l16",
1994                distribution,
1995                &points,
1996            );
1997            record_bulk_parameter_shape::<AsymmetricRStarSplit<32, 9, 16, 4>>(
1998                "bulk-b32-l16",
1999                distribution,
2000                &points,
2001            );
2002        }
2003    }
2004
2005    #[test]
2006    #[allow(clippy::too_many_lines)]
2007    fn records_inserted_bounded_parameter_shape() {
2008        const N: usize = 50_000;
2009
2010        for (distribution, points) in [
2011            ("uniform", uniform_points(N)),
2012            ("clustered", clustered_points(N)),
2013        ] {
2014            record_inserted_parameter_shape::<AsymmetricRStarSplit<4, 2, 4, 2>>(
2015                "inserted-rstar-split-a4-4",
2016                distribution,
2017                &points,
2018            );
2019            record_inserted_parameter_shape::<AsymmetricRStarSplit<4, 2, 8, 3>>(
2020                "inserted-rstar-split-a4-8",
2021                distribution,
2022                &points,
2023            );
2024            record_inserted_parameter_shape::<AsymmetricRStarSplit<4, 2, 16, 4>>(
2025                "inserted-rstar-split-a4-16",
2026                distribution,
2027                &points,
2028            );
2029            record_inserted_parameter_shape::<AsymmetricRStarSplit<4, 2, 32, 9>>(
2030                "inserted-rstar-split-a4-32",
2031                distribution,
2032                &points,
2033            );
2034            record_inserted_parameter_shape::<AsymmetricRStarSplit<6, 2, 8, 3>>(
2035                "inserted-rstar-split-a6-8",
2036                distribution,
2037                &points,
2038            );
2039            record_inserted_parameter_shape::<AsymmetricRStarSplit<6, 2, 10, 3>>(
2040                "inserted-rstar-split-a6-10",
2041                distribution,
2042                &points,
2043            );
2044            record_inserted_parameter_shape::<AsymmetricRStarSplit<6, 2, 12, 4>>(
2045                "inserted-rstar-split-a6-12",
2046                distribution,
2047                &points,
2048            );
2049            record_inserted_parameter_shape::<AsymmetricRStarSplit<6, 2, 14, 4>>(
2050                "inserted-rstar-split-a6-14",
2051                distribution,
2052                &points,
2053            );
2054            record_inserted_parameter_shape::<AsymmetricRStarSplit<6, 2, 16, 4>>(
2055                "inserted-rstar-split-a6-16",
2056                distribution,
2057                &points,
2058            );
2059            record_inserted_parameter_shape::<AsymmetricRStarSplit<6, 2, 32, 9>>(
2060                "inserted-rstar-split-a6-32",
2061                distribution,
2062                &points,
2063            );
2064            record_inserted_parameter_shape::<AsymmetricRStarSplit<8, 3, 8, 3>>(
2065                "inserted-rstar-split-a8-8",
2066                distribution,
2067                &points,
2068            );
2069            record_inserted_parameter_shape::<AsymmetricRStarSplit<8, 3, 10, 3>>(
2070                "inserted-rstar-split-a8-10",
2071                distribution,
2072                &points,
2073            );
2074            record_inserted_parameter_shape::<AsymmetricRStarSplit<8, 3, 12, 4>>(
2075                "inserted-rstar-split-a8-12",
2076                distribution,
2077                &points,
2078            );
2079            record_inserted_parameter_shape::<AsymmetricRStarSplit<8, 3, 16, 4>>(
2080                "inserted-rstar-split-a8-16",
2081                distribution,
2082                &points,
2083            );
2084            record_inserted_parameter_shape::<AsymmetricRStarSplit<12, 4, 16, 4>>(
2085                "inserted-rstar-split-a12-16",
2086                distribution,
2087                &points,
2088            );
2089            record_inserted_parameter_shape::<AsymmetricRStarSplit<12, 4, 32, 9>>(
2090                "inserted-rstar-split-a12-32",
2091                distribution,
2092                &points,
2093            );
2094            record_inserted_parameter_shape::<Quadratic<6, 2>>(
2095                "inserted-q6-6",
2096                distribution,
2097                &points,
2098            );
2099            record_inserted_parameter_shape::<Quadratic<8, 3>>(
2100                "inserted-q8-8",
2101                distribution,
2102                &points,
2103            );
2104            record_inserted_parameter_shape::<Quadratic<16, 4>>(
2105                "inserted-q16-16",
2106                distribution,
2107                &points,
2108            );
2109            record_inserted_parameter_shape::<Quadratic<32, 9>>(
2110                "inserted-q32-32",
2111                distribution,
2112                &points,
2113            );
2114            record_inserted_parameter_shape::<AsymmetricQuadratic<8, 3, 16, 4>>(
2115                "inserted-a8-16",
2116                distribution,
2117                &points,
2118            );
2119            record_inserted_parameter_shape::<AsymmetricQuadratic<8, 3, 32, 9>>(
2120                "inserted-a8-32",
2121                distribution,
2122                &points,
2123            );
2124            record_inserted_parameter_shape::<AsymmetricQuadratic<12, 4, 32, 9>>(
2125                "inserted-a12-32",
2126                distribution,
2127                &points,
2128            );
2129            record_inserted_parameter_shape::<AsymmetricRStarSplit<8, 3, 32, 9>>(
2130                "inserted-rstar-split-a8-32",
2131                distribution,
2132                &points,
2133            );
2134        }
2135    }
2136
2137    fn insert_built<Params: SplitParameters>(points: &[P]) -> Rtree<P, Params> {
2138        let mut tree: Rtree<P, Params> = Rtree::new();
2139        for p in points {
2140            tree.insert(*p);
2141        }
2142        tree
2143    }
2144
2145    fn checked_subtree_union(node: &Node<P>) -> Bounds {
2146        match node {
2147            Node::Leaf(leaf) => union_all(
2148                &leaf
2149                    .values()
2150                    .iter()
2151                    .map(Indexable::bounds)
2152                    .collect::<Vec<_>>(),
2153            ),
2154            Node::Branch(children) => {
2155                for (b, child) in children {
2156                    assert_eq!(*b, checked_subtree_union(child));
2157                }
2158                union_all(&children.iter().map(|(b, _)| *b).collect::<Vec<_>>())
2159            }
2160        }
2161    }
2162
2163    fn assert_fill_and_depth<Params: SplitParameters>(
2164        tree: &Rtree<P, Params>,
2165        inserted_len: usize,
2166    ) {
2167        fn walk<Params: SplitParameters>(
2168            node: &Node<P>,
2169            depth: usize,
2170            leaf_depths: &mut Vec<usize>,
2171        ) {
2172            match node {
2173                Node::Leaf(leaf) => {
2174                    assert!(leaf.len() <= Params::LEAF_MAX);
2175                    leaf_depths.push(depth);
2176                }
2177                Node::Branch(children) => {
2178                    assert!(children.len() <= Params::BRANCH_MAX);
2179                    for (_, child) in children {
2180                        walk::<Params>(child, depth + 1, leaf_depths);
2181                    }
2182                }
2183            }
2184        }
2185        let mut leaf_depths = Vec::new();
2186        walk::<Params>(&tree.root, 1, &mut leaf_depths);
2187        assert!(leaf_depths.iter().all(|&d| d == leaf_depths[0]));
2188        assert_eq!(tree.height(), leaf_depths[0]);
2189        assert_eq!(tree.height(), tree.root.height());
2190        assert_eq!(tree.root.value_count(), tree.len());
2191        assert_eq!(tree.len(), inserted_len);
2192    }
2193
2194    fn adversarial_bulk_inputs() -> [Vec<P>; 4] {
2195        let sorted_by_x: Vec<P> = (0..5_000i32)
2196            .map(|i| P::new(f64::from(i), f64::from(i % 71)))
2197            .collect();
2198        let reverse_sorted_by_x: Vec<P> = sorted_by_x.iter().copied().rev().collect();
2199        let one_point: Vec<P> = core::iter::repeat_n(P::new(123.0, 456.0), 5_000).collect();
2200        let vertical_line: Vec<P> = (0..5_000i32).map(|i| P::new(7.0, f64::from(i))).collect();
2201        [sorted_by_x, reverse_sorted_by_x, one_point, vertical_line]
2202    }
2203
2204    fn adversarial_str_invariant_case<Params: SplitParameters>() {
2205        for points in adversarial_bulk_inputs() {
2206            let bulk: Rtree<P, Params> = points.clone().into_iter().collect();
2207            checked_subtree_union(&bulk.root);
2208            assert_fill_and_depth(&bulk, points.len());
2209        }
2210    }
2211
2212    #[test]
2213    fn invariants_hold_on_adversarial_bulk_inputs_max6() {
2214        adversarial_str_invariant_case::<Quadratic<6, 2>>();
2215    }
2216
2217    #[test]
2218    fn invariants_hold_on_adversarial_bulk_inputs_max8() {
2219        adversarial_str_invariant_case::<Quadratic<8, 3>>();
2220    }
2221
2222    #[test]
2223    fn invariants_hold_on_adversarial_bulk_inputs_max16() {
2224        adversarial_str_invariant_case::<Quadratic<16, 4>>();
2225    }
2226
2227    #[test]
2228    fn invariants_hold_on_adversarial_bulk_inputs_max32() {
2229        adversarial_str_invariant_case::<Quadratic<32, 9>>();
2230    }
2231
2232    fn structural_invariant_case<Params: SplitParameters>() {
2233        let points = uniform_points(10_000);
2234        let tree = insert_built::<Params>(&points);
2235        checked_subtree_union(&tree.root);
2236        assert_fill_and_depth(&tree, points.len());
2237        let bulk: Rtree<P, Params> = points.clone().into_iter().collect();
2238        checked_subtree_union(&bulk.root);
2239        assert_fill_and_depth(&bulk, points.len());
2240    }
2241
2242    #[test]
2243    fn invariant_bounds_fill_and_depth_max6() {
2244        structural_invariant_case::<Quadratic<6, 2>>();
2245    }
2246
2247    #[test]
2248    fn invariant_bounds_fill_and_depth_max8() {
2249        structural_invariant_case::<Quadratic<8, 3>>();
2250    }
2251
2252    #[test]
2253    fn invariant_bounds_fill_and_depth_max16() {
2254        structural_invariant_case::<Quadratic<16, 4>>();
2255    }
2256
2257    #[test]
2258    fn invariant_bounds_fill_and_depth_max32() {
2259        structural_invariant_case::<Quadratic<32, 9>>();
2260    }
2261
2262    #[test]
2263    fn invariant_bounds_fill_and_depth_asymmetric_8_32() {
2264        structural_invariant_case::<AsymmetricQuadratic<8, 3, 32, 9>>();
2265    }
2266
2267    #[test]
2268    fn query_of_an_exact_leaf_box_matches_scan() {
2269        fn collect_leaf_boxes(node: &Node<P>, boxes: &mut Vec<Bounds>) {
2270            match node {
2271                Node::Leaf(values) => boxes.push(
2272                    values
2273                        .iter()
2274                        .map(Indexable::bounds)
2275                        .reduce(|left, right| left.union(&right))
2276                        .expect("bulk leaves are non-empty"),
2277                ),
2278                Node::Branch(children) => {
2279                    for (_, child) in children {
2280                        collect_leaf_boxes(child, boxes);
2281                    }
2282                }
2283            }
2284        }
2285
2286        let points = uniform_points(40);
2287        let tree: Rtree<P> = points.clone().into_iter().collect();
2288        let mut leaf_boxes = Vec::new();
2289        collect_leaf_boxes(&tree.root, &mut leaf_boxes);
2290        for leaf_box in leaf_boxes {
2291            let mut expected: Vec<[f64; 2]> = points
2292                .iter()
2293                .filter(|p| {
2294                    p.get::<0>() >= leaf_box.min[0]
2295                        && p.get::<0>() <= leaf_box.max[0]
2296                        && p.get::<1>() >= leaf_box.min[1]
2297                        && p.get::<1>() <= leaf_box.max[1]
2298                })
2299                .map(|p| [p.get::<0>(), p.get::<1>()])
2300                .collect();
2301            expected.sort_by(coordinate_order);
2302            for predicate in [
2303                Predicate::Intersects(leaf_box),
2304                Predicate::CoveredBy(leaf_box),
2305            ] {
2306                let mut got: Vec<[f64; 2]> = tree
2307                    .query(predicate)
2308                    .iter()
2309                    .map(|p| [p.get::<0>(), p.get::<1>()])
2310                    .collect();
2311                got.sort_by(coordinate_order);
2312                assert_eq!(
2313                    got, expected,
2314                    "query of an exact leaf box diverges from the scan for {predicate:?}"
2315                );
2316            }
2317        }
2318    }
2319
2320    fn coordinate_order(a: &[f64; 2], b: &[f64; 2]) -> core::cmp::Ordering {
2321        a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1]))
2322    }
2323
2324    #[test]
2325    fn bulk_load_balances() {
2326        let points: Vec<P> = (0..10_000)
2327            .map(|i| P::new(f64::from(i % 100), f64::from(i / 100)))
2328            .collect();
2329        let t: Rtree<P> = points.into_iter().collect();
2330        assert_eq!(t.len(), 10_000);
2331        // Four-way bulk packing needs seven levels to cover 10k values
2332        // (4^6 values beneath a height-7 root).
2333        assert_eq!(t.height(), 7);
2334    }
2335}