@@ -30,7 +30,8 @@ use query::{
3030 batch:: {
3131 par_bbox_filter, par_contains, par_knn, par_knn_to_polygons, par_knn_to_polygons_sorted,
3232 par_knn_with_delta, par_points_within_distance_of_polygon, par_polygon_intersects_join,
33- par_within_distance, par_within_distance_flipped, par_within_distance_to_polygons,
33+ par_radius, par_within_distance, par_within_distance_flipped,
34+ par_within_distance_to_polygons,
3435 } ,
3536 geometry:: { convex_hull_area, polygon_area, polygon_intersection_area} ,
3637 multipoly:: { dedup_indices, dedup_self_pairs, polygon_parts_csr, sum_part_areas} ,
@@ -643,6 +644,85 @@ impl Engine {
643644 Ok ( result)
644645 }
645646
647+ /// Engine point indices within `distance` (Euclidean) of the center (cx, cy)
648+ fn radius_query < ' py > (
649+ & mut self ,
650+ py : Python < ' py > ,
651+ cx : f64 ,
652+ cy : f64 ,
653+ distance : f64 ,
654+ ) -> PyResult < Bound < ' py , PyArray1 < u64 > > > {
655+ if self . ring_offsets . is_some ( ) {
656+ return Err ( PyValueError :: new_err (
657+ "radius_query requires a point dataset" ,
658+ ) ) ;
659+ }
660+ let bbox = Rect :: new (
661+ coord ! { x: cx - distance, y: cy - distance } ,
662+ coord ! { x: cx + distance, y: cy + distance } ,
663+ ) ;
664+ // Skip histogram early-exit when delta is non-empty: delta points are not in the histogram
665+ if self . delta_xs . is_empty ( ) {
666+ if let Some ( hist) = & self . stats . histogram {
667+ if !hist. has_any_in_bbox ( & bbox) {
668+ let empty: Vec < u64 > = Vec :: new ( ) ;
669+ return Ok ( PyArray1 :: from_vec ( py, empty) ) ;
670+ }
671+ }
672+ }
673+ let kind = self . plan_index ( & Query :: Range { bbox } , 1 ) ;
674+ self . build_index_if_needed ( kind) ;
675+ let mut result = match kind {
676+ IndexKind :: BruteForce => par_radius (
677+ self . brute . as_ref ( ) . unwrap ( ) ,
678+ & self . xs ,
679+ & self . ys ,
680+ cx,
681+ cy,
682+ distance,
683+ ) ,
684+ IndexKind :: RTree => par_radius (
685+ self . rtree . as_ref ( ) . unwrap ( ) ,
686+ & self . xs ,
687+ & self . ys ,
688+ cx,
689+ cy,
690+ distance,
691+ ) ,
692+ IndexKind :: KdTree => par_radius (
693+ self . kdtree . as_ref ( ) . unwrap ( ) ,
694+ & self . xs ,
695+ & self . ys ,
696+ cx,
697+ cy,
698+ distance,
699+ ) ,
700+ IndexKind :: Grid => par_radius (
701+ self . grid . as_ref ( ) . unwrap ( ) ,
702+ & self . xs ,
703+ & self . ys ,
704+ cx,
705+ cy,
706+ distance,
707+ ) ,
708+ } ;
709+ // Refine delta points by exact distance and address them in the combined index space
710+ if !self . delta_xs . is_empty ( ) {
711+ let d2 = distance * distance;
712+ let n_main = self . xs . len ( ) ;
713+ for ( i, ( & x, & y) ) in self . delta_xs . iter ( ) . zip ( self . delta_ys . iter ( ) ) . enumerate ( ) {
714+ let dx = x - cx;
715+ let dy = y - cy;
716+ if dx * dx + dy * dy <= d2 {
717+ result. push ( ( n_main + i) as u64 ) ;
718+ }
719+ }
720+ self . delta_query_cost += self . delta_xs . len ( ) as u64 ;
721+ self . maybe_flush_on_cost ( kind) ;
722+ }
723+ Ok ( PyArray1 :: from_vec ( py, result) )
724+ }
725+
646726 /// Intersect multiple bbox queries via two-pointer sorted merge on the hit arrays.
647727 /// O(K * |H|) over K predicates and per-predicate hit count |H| rather than O(K * N)
648728 /// bitmap ANDs. Short-circuits once the running intersection is empty.
0 commit comments