Skip to main content

geometry_rtree/
split.rs

1//! Node-split strategies — the type parameter that governs tree shape.
2//!
3//! Mirrors `boost/geometry/index/parameters.hpp` and the
4//! `index/detail/rtree/{linear,quadratic,rstar}/redistribute_elements.hpp`
5//! family. When a node overflows, the split strategy decides how to
6//! partition its children into two nodes. Boost exposes this as a
7//! template parameter; the port uses a [`SplitParameters`] trait
8//! carried on `Rtree<T, Params>`.
9//!
10//! Four strategies ship: [`AsymmetricRStarSplit`] (the [`crate::Rtree`]
11//! default), symmetric [`RStarSplit`], [`Quadratic`] (Boost's textbook
12//! split), and [`Linear`] (cheaper inserts, looser trees).
13//!
14//! # Choosing parameters
15//!
16//! Start with [`Rtree<T>`](crate::Rtree). Its default was selected from
17//! uniform and clustered workloads covering bulk loading, insertion, range
18//! queries, and nearest-neighbour queries. A different data distribution,
19//! payload size, construction method, or query mix can change the best tree
20//! shape, so customize the parameters only after benchmarking a
21//! representative workload.
22//!
23//! The default is
24//! `AsymmetricRStarSplit<6, 2, 12, 4, 4, 4>`. The parameters are positional:
25//!
26//! | Parameter | Used by | General trade-off |
27//! |-----------|---------|-------------------|
28//! | `BRANCH_MAX` | [`Rtree::insert`](crate::Rtree::insert) | Smaller branches evaluate fewer sibling bounds; larger branches make a shallower tree. |
29//! | `BRANCH_MIN` | Branch splitting after insertion | Higher values keep branches denser; lower values give the split more freedom. |
30//! | `LEAF_MAX` | [`Rtree::insert`](crate::Rtree::insert) | Smaller leaves test fewer values when reached; larger leaves reduce tree structure. |
31//! | `LEAF_MIN` | Leaf splitting after insertion | Controls the minimum fill of each new leaf. |
32//! | `PACKED_BRANCH_MAX` | [`FromIterator`] | Controls branch fanout in the initial bulk-packed tree. |
33//! | `PACKED_LEAF_MAX` | [`FromIterator`] | Controls how many values an initial packed leaf may contain. |
34//!
35//! The packed capacities affect only the layout initially produced by
36//! [`FromIterator`]. Later insertions use the first
37//! four insertion capacities; existing packed nodes are not rebuilt. A zero
38//! packed capacity means "inherit the corresponding insertion maximum", so a
39//! four-parameter `AsymmetricRStarSplit` retains the traditional behavior.
40//!
41//! ```
42//! use geometry_rtree::{AsymmetricRStarSplit, Bounds, Rtree};
43//!
44//! // Spell out the measured default so each positional parameter is visible.
45//! type Parameters = AsymmetricRStarSplit<
46//!     6,  // insertion branch maximum
47//!     2,  // insertion branch minimum
48//!     12, // insertion leaf maximum
49//!     4,  // insertion leaf minimum
50//!     4,  // bulk-packed branch maximum
51//!     4,  // bulk-packed leaf maximum
52//! >;
53//!
54//! let tree: Rtree<Bounds, Parameters> = [
55//!     Bounds::point([0.0, 0.0]),
56//!     Bounds::point([1.0, 1.0]),
57//! ]
58//! .into_iter()
59//! .collect();
60//! assert_eq!(tree.len(), 2);
61//! ```
62//!
63//! ## Tuning procedure
64//!
65//! - For a read-mostly tree built with `collect`, tune the two packed maxima
66//!   first.
67//! - For a frequently modified tree, tune the insertion maxima and minima
68//!   first.
69//! - Smaller maxima can reduce the work of nearest-neighbour searches, but
70//!   create more nodes and may increase tree height.
71//! - Larger maxima can reduce structural and traversal overhead, but make
72//!   each visited branch or leaf more expensive to scan.
73//! - Measure construction together with representative calls to
74//!   [`query`](crate::Rtree::query), [`nearest`](crate::Rtree::nearest), or
75//!   [`nearest_iter`](crate::Rtree::nearest_iter). Improving one workload can
76//!   regress another.
77//!
78//! The built-in insertion strategies expect both minimums to be non-zero and
79//! to leave room for two groups when a node of `MAX + 1` entries splits:
80//! `2 * BRANCH_MIN <= BRANCH_MAX + 1` and
81//! `2 * LEAF_MIN <= LEAF_MAX + 1`. Bulk leaf capacity must be non-zero and
82//! bulk branch capacity must be at least two; bulk loading checks those two
83//! constraints.
84//!
85//! ## Why these defaults
86//!
87//! The retained defaults were measured on 2026-07-13 with iai-callgrind,
88//! 50,000 two-dimensional points, 100 queries, and `k = 8`. Candidate
89//! configurations were evaluated across inserted and bulk-built trees,
90//! uniform and clustered distributions, construction cost, bounded and
91//! streaming kNN, and range queries. The insertion capacities `6/2/12/4`
92//! and packed capacities `4/4` were retained as the best balanced measured
93//! configuration across that matrix. This evidence supports the general
94//! default; it is not a guarantee for every workload.
95
96use alloc::vec::Vec;
97
98use crate::bounds::{Bounds, union_all};
99
100/// How a full node is partitioned when it overflows.
101///
102/// Mirrors `index::parameters` + the split policy
103/// (`index/parameters.hpp`). Insertion uses the leaf/branch maxima and
104/// minima; bulk loading may select smaller maxima independently. The
105/// `split` method takes overflowing children (as bounds, so the strategy
106/// is payload-agnostic) and returns the two groups by index. Bulk leaf
107/// capacity must be non-zero and bulk branch capacity must be at least two.
108pub trait SplitParameters {
109    /// Maximum children per node before it must split.
110    const MAX: usize;
111    /// Minimum children each node must keep after a split.
112    const MIN: usize;
113    /// Maximum values in a leaf. Defaults to [`Self::MAX`].
114    const LEAF_MAX: usize = Self::MAX;
115    /// Minimum values in either half of a split leaf.
116    const LEAF_MIN: usize = Self::MIN;
117    /// Maximum children in a branch. Defaults to [`Self::MAX`].
118    const BRANCH_MAX: usize = Self::MAX;
119    /// Minimum children in either half of a split branch.
120    const BRANCH_MIN: usize = Self::MIN;
121    /// Maximum values per leaf created by bulk loading.
122    const BULK_LEAF_MAX: usize = Self::LEAF_MAX;
123    /// Maximum children per branch created by bulk loading.
124    const BULK_BRANCH_MAX: usize = Self::BRANCH_MAX;
125
126    /// Partition `entries` (a bounds-per-child list) into two groups of
127    /// indices, each of size at least [`Self::MIN`].
128    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>);
129
130    /// Partition an overflowing leaf.
131    #[must_use]
132    fn split_leaf(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
133        Self::split(entries)
134    }
135
136    /// Partition an overflowing branch.
137    #[must_use]
138    fn split_branch(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
139        Self::split(entries)
140    }
141}
142
143/// Quadratic split — Boost's textbook default.
144///
145/// `O(n²)`: pick the two children whose combined bounding box wastes the
146/// most area as the seeds of the two groups, then assign each remaining
147/// child to whichever group's box it enlarges least. Mirrors
148/// `index/detail/rtree/quadratic/redistribute_elements.hpp`.
149///
150/// The type's own `<32, 9>` default is a symmetric alternative; it is not the
151/// default strategy of [`crate::Rtree`]. Treat an explicit strategy choice as
152/// a workload-specific opt-in and follow the [module tuning guide](self).
153#[derive(Debug, Clone, Copy, Default)]
154pub struct Quadratic<const MAX: usize = 32, const MIN: usize = 9>;
155
156impl<const MAX: usize, const MIN: usize> SplitParameters for Quadratic<MAX, MIN> {
157    const MAX: usize = MAX;
158    const MIN: usize = MIN;
159
160    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
161        quadratic_partition(entries, MIN)
162    }
163}
164
165/// Quadratic split with independent branch and leaf capacities.
166///
167/// This keeps broad leaves for cheap bulk loading and subtree dumps,
168/// while narrower branches reduce the child volume expanded by nearest
169/// searches. The four const parameters are branch maximum/minimum,
170/// followed by leaf maximum/minimum.
171#[derive(Debug, Clone, Copy, Default)]
172pub struct AsymmetricQuadratic<
173    const BRANCH_MAX: usize,
174    const BRANCH_MIN: usize,
175    const LEAF_MAX: usize,
176    const LEAF_MIN: usize,
177>;
178
179impl<const BRANCH_MAX: usize, const BRANCH_MIN: usize, const LEAF_MAX: usize, const LEAF_MIN: usize>
180    SplitParameters for AsymmetricQuadratic<BRANCH_MAX, BRANCH_MIN, LEAF_MAX, LEAF_MIN>
181{
182    const MAX: usize = BRANCH_MAX;
183    const MIN: usize = BRANCH_MIN;
184    const LEAF_MAX: usize = LEAF_MAX;
185    const LEAF_MIN: usize = LEAF_MIN;
186    const BRANCH_MAX: usize = BRANCH_MAX;
187    const BRANCH_MIN: usize = BRANCH_MIN;
188
189    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
190        quadratic_partition(entries, BRANCH_MIN)
191    }
192
193    fn split_leaf(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
194        quadratic_partition(entries, LEAF_MIN)
195    }
196}
197
198/// R\*-split selection with one capacity for branches and leaves.
199///
200/// Chooses the split axis by minimum summed margin, then the split index
201/// by minimum overlap and combined area. Insertion descent still uses
202/// least enlargement; this policy does not perform forced reinsertion. Its
203/// `<32, 9>` type default is not the default of [`crate::Rtree`]; see the
204/// [module tuning guide](self).
205#[derive(Debug, Clone, Copy, Default)]
206pub struct RStarSplit<const MAX: usize = 32, const MIN: usize = 9>;
207
208impl<const MAX: usize, const MIN: usize> SplitParameters for RStarSplit<MAX, MIN> {
209    const MAX: usize = MAX;
210    const MIN: usize = MIN;
211
212    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
213        rstar_partition(entries, MIN)
214    }
215}
216
217/// R\*-split selection with independent insertion and bulk capacities.
218///
219/// The first four parameters are insertion branch maximum/minimum and
220/// leaf maximum/minimum. Optional `PACKED_BRANCH_MAX` and
221/// `PACKED_LEAF_MAX` parameters set the top-down bulk layout; zero (the
222/// default) inherits the corresponding insertion maximum. See the
223/// [module tuning guide](self) before selecting non-default values.
224#[derive(Debug, Clone, Copy, Default)]
225pub struct AsymmetricRStarSplit<
226    const BRANCH_MAX: usize,
227    const BRANCH_MIN: usize,
228    const LEAF_MAX: usize,
229    const LEAF_MIN: usize,
230    const PACKED_BRANCH_MAX: usize = 0,
231    const PACKED_LEAF_MAX: usize = 0,
232>;
233
234impl<
235    const BRANCH_MAX: usize,
236    const BRANCH_MIN: usize,
237    const LEAF_MAX: usize,
238    const LEAF_MIN: usize,
239    const PACKED_BRANCH_MAX: usize,
240    const PACKED_LEAF_MAX: usize,
241> SplitParameters
242    for AsymmetricRStarSplit<
243        BRANCH_MAX,
244        BRANCH_MIN,
245        LEAF_MAX,
246        LEAF_MIN,
247        PACKED_BRANCH_MAX,
248        PACKED_LEAF_MAX,
249    >
250{
251    const MAX: usize = BRANCH_MAX;
252    const MIN: usize = BRANCH_MIN;
253    const LEAF_MAX: usize = LEAF_MAX;
254    const LEAF_MIN: usize = LEAF_MIN;
255    const BRANCH_MAX: usize = BRANCH_MAX;
256    const BRANCH_MIN: usize = BRANCH_MIN;
257    const BULK_LEAF_MAX: usize = if PACKED_LEAF_MAX == 0 {
258        LEAF_MAX
259    } else {
260        PACKED_LEAF_MAX
261    };
262    const BULK_BRANCH_MAX: usize = if PACKED_BRANCH_MAX == 0 {
263        BRANCH_MAX
264    } else {
265        PACKED_BRANCH_MAX
266    };
267
268    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
269        rstar_partition(entries, BRANCH_MIN)
270    }
271
272    fn split_leaf(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
273        rstar_partition(entries, LEAF_MIN)
274    }
275}
276
277fn quadratic_partition(entries: &[Bounds], min: usize) -> (Vec<usize>, Vec<usize>) {
278    let n = entries.len();
279    let (s1, s2) = quadratic_seeds(entries);
280
281    let mut g1 = Vec::from([s1]);
282    let mut g2 = Vec::from([s2]);
283    let mut b1 = entries[s1];
284    let mut b2 = entries[s2];
285
286    let mut remaining: Vec<usize> = (0..n).filter(|&i| i != s1 && i != s2).collect();
287
288    while !remaining.is_empty() {
289        // Force a group to fill if every remaining entry is needed to
290        // reach MIN.
291        if g1.len() + remaining.len() == min {
292            for idx in remaining.drain(..) {
293                g1.push(idx);
294                b1 = b1.union(&entries[idx]);
295            }
296            break;
297        }
298        if g2.len() + remaining.len() == min {
299            for idx in remaining.drain(..) {
300                g2.push(idx);
301                b2 = b2.union(&entries[idx]);
302            }
303            break;
304        }
305
306        // Guttman's PickNext: decide the entry with the strongest
307        // preference first, rather than depending on input order.
308        let mut next_pos = 0;
309        let mut best_difference = f64::NEG_INFINITY;
310        for (pos, &idx) in remaining.iter().enumerate() {
311            let e1 = b1.enlargement(&entries[idx]);
312            let e2 = b2.enlargement(&entries[idx]);
313            let difference = (e1 - e2).abs();
314            if difference > best_difference {
315                next_pos = pos;
316                best_difference = difference;
317            }
318        }
319        let idx = remaining.swap_remove(next_pos);
320        let e1 = b1.enlargement(&entries[idx]);
321        let e2 = b2.enlargement(&entries[idx]);
322        let assign_first = match e1.total_cmp(&e2) {
323            core::cmp::Ordering::Less => true,
324            core::cmp::Ordering::Greater => false,
325            core::cmp::Ordering::Equal => match b1.area().total_cmp(&b2.area()) {
326                core::cmp::Ordering::Less => true,
327                core::cmp::Ordering::Greater => false,
328                core::cmp::Ordering::Equal => g1.len() <= g2.len(),
329            },
330        };
331        if assign_first {
332            g1.push(idx);
333            b1 = b1.union(&entries[idx]);
334        } else {
335            g2.push(idx);
336            b2 = b2.union(&entries[idx]);
337        }
338    }
339
340    (g1, g2)
341}
342
343#[allow(
344    clippy::float_cmp,
345    reason = "exact R*-split tie-break between equal overlap and area"
346)]
347fn rstar_partition(entries: &[Bounds], min: usize) -> (Vec<usize>, Vec<usize>) {
348    let mut best_axis = 0;
349    let mut best_margin = f64::INFINITY;
350    for axis in 0..2 {
351        let ordered = sorted_indices(entries, axis);
352        let suffixes = suffix_bounds(entries, &ordered);
353        let mut left = partition_bounds(entries, &ordered[..min]);
354        let mut margin = left.half_perimeter() + suffixes[min].half_perimeter();
355        for split in (min + 1)..=entries.len() - min {
356            left = left.union(&entries[ordered[split - 1]]);
357            margin += left.half_perimeter() + suffixes[split].half_perimeter();
358        }
359        if margin < best_margin {
360            best_axis = axis;
361            best_margin = margin;
362        }
363    }
364
365    let ordered = sorted_indices(entries, best_axis);
366    let suffixes = suffix_bounds(entries, &ordered);
367    let mut best_split = min;
368    let mut best_overlap = f64::INFINITY;
369    let mut best_area = f64::INFINITY;
370    let mut left = partition_bounds(entries, &ordered[..min]);
371    for split in min..=entries.len() - min {
372        if split > min {
373            left = left.union(&entries[ordered[split - 1]]);
374        }
375        let right = suffixes[split];
376        let overlap = intersection_area(&left, &right);
377        let area = left.area() + right.area();
378        if overlap < best_overlap || (overlap == best_overlap && area < best_area) {
379            best_split = split;
380            best_overlap = overlap;
381            best_area = area;
382        }
383    }
384
385    (
386        ordered[..best_split].to_vec(),
387        ordered[best_split..].to_vec(),
388    )
389}
390
391fn sorted_indices(entries: &[Bounds], axis: usize) -> Vec<usize> {
392    let mut indices: Vec<usize> = (0..entries.len()).collect();
393    indices.sort_unstable_by(|&left, &right| {
394        entries[left].min[axis].total_cmp(&entries[right].min[axis])
395    });
396    indices
397}
398
399fn partition_bounds(entries: &[Bounds], indices: &[usize]) -> Bounds {
400    let mut bounds = entries[indices[0]];
401    for &index in &indices[1..] {
402        bounds = bounds.union(&entries[index]);
403    }
404    bounds
405}
406
407fn suffix_bounds(entries: &[Bounds], indices: &[usize]) -> Vec<Bounds> {
408    let mut suffixes = Vec::with_capacity(indices.len());
409    let mut bounds = entries[*indices.last().expect("a split has at least two entries")];
410    suffixes.push(bounds);
411    for &index in indices[..indices.len() - 1].iter().rev() {
412        bounds = bounds.union(&entries[index]);
413        suffixes.push(bounds);
414    }
415    suffixes.reverse();
416    suffixes
417}
418
419fn intersection_area(left: &Bounds, right: &Bounds) -> f64 {
420    let width = left.max[0].min(right.max[0]) - left.min[0].max(right.min[0]);
421    let height = left.max[1].min(right.max[1]) - left.min[1].max(right.min[1]);
422    width.max(0.0) * height.max(0.0)
423}
424
425/// Linear split — cheap inserts, looser trees.
426///
427/// `O(n)`: pick the two children furthest apart along the axis of
428/// greatest spread as seeds, then distribute the rest greedily by
429/// least enlargement. Mirrors
430/// `index/detail/rtree/linear/redistribute_elements.hpp`.
431///
432/// Its `<32, 9>` type default is a symmetric alternative, not the default of
433/// [`crate::Rtree`]. See the [module tuning guide](self).
434#[derive(Debug, Clone, Copy, Default)]
435pub struct Linear<const MAX: usize = 32, const MIN: usize = 9>;
436
437impl<const MAX: usize, const MIN: usize> SplitParameters for Linear<MAX, MIN> {
438    const MAX: usize = MAX;
439    const MIN: usize = MIN;
440
441    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
442        let n = entries.len();
443        let (s1, s2) = linear_seeds(entries);
444
445        let mut g1 = Vec::from([s1]);
446        let mut g2 = Vec::from([s2]);
447        let mut b1 = entries[s1];
448        let mut b2 = entries[s2];
449
450        let mut remaining: Vec<usize> = (0..n).filter(|&i| i != s1 && i != s2).collect();
451        while let Some(idx) = remaining.pop() {
452            if g1.len() + remaining.len() + 1 == MIN {
453                g1.push(idx);
454                b1 = b1.union(&entries[idx]);
455                continue;
456            }
457            if g2.len() + remaining.len() + 1 == MIN {
458                g2.push(idx);
459                b2 = b2.union(&entries[idx]);
460                continue;
461            }
462            let e1 = b1.enlargement(&entries[idx]);
463            let e2 = b2.enlargement(&entries[idx]);
464            if e1 <= e2 {
465                g1.push(idx);
466                b1 = b1.union(&entries[idx]);
467            } else {
468                g2.push(idx);
469                b2 = b2.union(&entries[idx]);
470            }
471        }
472        (g1, g2)
473    }
474}
475
476/// The pair of children whose combined box wastes the most area — the
477/// quadratic-split seeds (`PickSeeds` in Boost's terms).
478fn quadratic_seeds(entries: &[Bounds]) -> (usize, usize) {
479    let mut worst = (0, 1, f64::NEG_INFINITY);
480    for i in 0..entries.len() {
481        for j in (i + 1)..entries.len() {
482            let combined = entries[i].union(&entries[j]).area();
483            let waste = combined - entries[i].area() - entries[j].area();
484            if waste > worst.2 {
485                worst = (i, j, waste);
486            }
487        }
488    }
489    (worst.0, worst.1)
490}
491
492/// The two children furthest apart along the widest axis — the
493/// linear-split seeds.
494fn linear_seeds(entries: &[Bounds]) -> (usize, usize) {
495    let all = union_all(entries);
496    let width = [all.max[0] - all.min[0], all.max[1] - all.min[1]];
497    let axis = usize::from(width[1] > width[0]);
498
499    // Extreme children: lowest high-side and highest low-side along axis.
500    let mut lo_idx = 0;
501    let mut hi_idx = 0;
502    let mut max_low = f64::NEG_INFINITY;
503    let mut min_high = f64::INFINITY;
504    for (i, b) in entries.iter().enumerate() {
505        if b.min[axis] > max_low {
506            max_low = b.min[axis];
507            hi_idx = i;
508        }
509        if b.max[axis] < min_high {
510            min_high = b.max[axis];
511            lo_idx = i;
512        }
513    }
514    if lo_idx == hi_idx {
515        // Degenerate: fall back to the first two.
516        (0, usize::from(entries.len() > 1))
517    } else {
518        (lo_idx, hi_idx)
519    }
520}
521
522#[cfg(test)]
523#[allow(
524    clippy::cast_precision_loss,
525    reason = "small test indices convert exactly to f64"
526)]
527mod tests {
528    use super::{
529        AsymmetricQuadratic, AsymmetricRStarSplit, Linear, Quadratic, RStarSplit, SplitParameters,
530    };
531    use crate::bounds::Bounds;
532
533    fn line_of_boxes(n: usize) -> Vec<Bounds> {
534        (0..n)
535            .map(|i| Bounds::point([i as f64, 0.0]))
536            .collect::<Vec<_>>()
537    }
538
539    #[test]
540    fn quadratic_splits_into_two_min_sized_groups() {
541        let entries = line_of_boxes(9);
542        let (g1, g2) = <Quadratic<8, 3>>::split(&entries);
543        assert!(g1.len() >= 3 && g2.len() >= 3);
544        assert_eq!(g1.len() + g2.len(), 9);
545    }
546
547    #[test]
548    fn linear_splits_into_two_min_sized_groups() {
549        let entries = line_of_boxes(9);
550        let (g1, g2) = <Linear<8, 3>>::split(&entries);
551        assert!(g1.len() >= 3 && g2.len() >= 3);
552        assert_eq!(g1.len() + g2.len(), 9);
553    }
554
555    #[test]
556    fn rstar_splits_into_two_min_sized_groups() {
557        let entries = line_of_boxes(9);
558        let (g1, g2) = <RStarSplit<8, 3>>::split(&entries);
559        assert!(g1.len() >= 3 && g2.len() >= 3);
560        assert_eq!(g1.len() + g2.len(), 9);
561    }
562
563    #[test]
564    fn every_index_assigned_exactly_once() {
565        let entries = line_of_boxes(9);
566        let (mut g1, g2) = <Quadratic<8, 3>>::split(&entries);
567        g1.extend(g2);
568        g1.sort_unstable();
569        assert_eq!(g1, (0..9).collect::<Vec<_>>());
570    }
571
572    #[test]
573    fn asymmetric_leaf_and_branch_minimums_are_independent() {
574        type Params = AsymmetricQuadratic<8, 3, 32, 9>;
575        let branch_entries = line_of_boxes(9);
576        let (b1, b2) = Params::split_branch(&branch_entries);
577        assert!(b1.len() >= Params::BRANCH_MIN && b2.len() >= Params::BRANCH_MIN);
578
579        let leaf_entries = line_of_boxes(33);
580        let (l1, l2) = Params::split_leaf(&leaf_entries);
581        assert!(l1.len() >= Params::LEAF_MIN && l2.len() >= Params::LEAF_MIN);
582    }
583
584    #[test]
585    fn asymmetric_rstar_leaf_and_branch_minimums_are_independent() {
586        type Params = AsymmetricRStarSplit<8, 3, 32, 9>;
587        let branch_entries = line_of_boxes(9);
588        let (b1, b2) = Params::split_branch(&branch_entries);
589        assert!(b1.len() >= Params::BRANCH_MIN && b2.len() >= Params::BRANCH_MIN);
590
591        let leaf_entries = line_of_boxes(33);
592        let (l1, l2) = Params::split_leaf(&leaf_entries);
593        assert!(l1.len() >= Params::LEAF_MIN && l2.len() >= Params::LEAF_MIN);
594    }
595
596    #[test]
597    fn asymmetric_rstar_bulk_capacities_are_independent_and_optional() {
598        type Inherited = AsymmetricRStarSplit<6, 2, 12, 4>;
599        type Tuned = AsymmetricRStarSplit<6, 2, 12, 4, 4, 4>;
600
601        assert_eq!(Inherited::BULK_BRANCH_MAX, 6);
602        assert_eq!(Inherited::BULK_LEAF_MAX, 12);
603
604        assert_eq!(Tuned::BRANCH_MAX, 6);
605        assert_eq!(Tuned::LEAF_MAX, 12);
606        assert_eq!(Tuned::BULK_BRANCH_MAX, 4);
607        assert_eq!(Tuned::BULK_LEAF_MAX, 4);
608    }
609}