@@ -391,26 +391,30 @@ def query_points(self, x, y, z=None, k=None):
391391 # Query KD-tree adaptively to get k unique cells
392392 candidates = self ._get_k_unique_cells (point , k )
393393
394+ # Collect all matching cells for tie-breaking
395+ matching_cells = []
396+
394397 # Test candidates in order (nearest first)
395398 for cellid in candidates :
396399 if self .is_3d :
397400 # 3D: check 3D bounding box
398401 if self ._point_in_cell_3d (point , cellid ):
399- results [i ] = cellid
400- break
402+ matching_cells .append (cellid )
401403 else :
402404 # 2D: use ConvexHull test
403405 if self ._point_in_cell_vectorized (point [:2 ], cellid ):
404406 # Found 2D cell, now check z if provided
405407 if z is None :
406- results [i ] = cellid
407- break
408+ matching_cells .append (cellid )
408409 else :
409410 # Search through layers to find the right z
410411 cell_3d = self ._find_layer_for_z (cellid , z [i ])
411412 if cell_3d is not None :
412- results [i ] = cell_3d
413- break
413+ matching_cells .append (cell_3d )
414+
415+ # Apply grid-specific tie-breaking when multiple cells match
416+ if len (matching_cells ) > 0 :
417+ results [i ] = self ._apply_tiebreaker (matching_cells )
414418
415419 return results
416420
@@ -534,6 +538,87 @@ def _point_in_cell_3d(self, point, cellid):
534538 and bbox [4 ] - self .epsilon <= point [2 ] <= bbox [5 ] + self .epsilon
535539 )
536540
541+ def _is_on_boundary_2d (self , point , cellid ):
542+ """
543+ Check if a 2D point is on the boundary of a cell.
544+
545+ A point is considered on the boundary if it's very close (< 1e-9) to any edge.
546+ Uses a much tighter tolerance than epsilon to detect true boundary cases.
547+ """
548+ bbox = self .bounding_boxes [cellid ]
549+ boundary_tol = 1e-9 # Much tighter than epsilon for exact boundary detection
550+ # Check if point is on any bounding box edge
551+ on_x_edge = (
552+ abs (point [0 ] - bbox [0 ]) < boundary_tol or
553+ abs (point [0 ] - bbox [1 ]) < boundary_tol
554+ )
555+ on_y_edge = (
556+ abs (point [1 ] - bbox [2 ]) < boundary_tol or
557+ abs (point [1 ] - bbox [3 ]) < boundary_tol
558+ )
559+ return on_x_edge or on_y_edge
560+
561+ def _is_on_boundary_3d (self , point , cellid ):
562+ """
563+ Check if a 3D point is on the boundary of a cell.
564+
565+ A point is considered on the boundary if it's very close (< 1e-9) to any face.
566+ Uses a much tighter tolerance than epsilon to detect true boundary cases.
567+ """
568+ bbox = self .bounding_boxes_3d [cellid ]
569+ boundary_tol = 1e-9 # Much tighter than epsilon for exact boundary detection
570+ # Check if point is on any bounding box face
571+ on_x_face = (
572+ abs (point [0 ] - bbox [0 ]) < boundary_tol or
573+ abs (point [0 ] - bbox [1 ]) < boundary_tol
574+ )
575+ on_y_face = (
576+ abs (point [1 ] - bbox [2 ]) < boundary_tol or
577+ abs (point [1 ] - bbox [3 ]) < boundary_tol
578+ )
579+ on_z_face = (
580+ abs (point [2 ] - bbox [4 ]) < boundary_tol or
581+ abs (point [2 ] - bbox [5 ]) < boundary_tol
582+ )
583+ return on_x_face or on_y_face or on_z_face
584+
585+ def _apply_tiebreaker (self , matching_cells ):
586+ """
587+ Apply grid-specific tie-breaking when multiple cells match.
588+
589+ Implements the same tie-breaking logic as the original grid implementations:
590+ - StructuredGrid: cell with lowest row, then lowest column
591+ - VertexGrid/UnstructuredGrid: cell with lowest cell ID
592+
593+ Parameters
594+ ----------
595+ matching_cells : list
596+ List of cell indices that all contain the point
597+
598+ Returns
599+ -------
600+ cellid : int
601+ The selected cell after tie-breaking
602+ """
603+ if len (matching_cells ) == 1 :
604+ return matching_cells [0 ]
605+
606+ if self .grid .grid_type == "structured" :
607+ # For structured grids: choose cell with lowest row, then lowest column
608+ # Convert cell indices to (row, col) tuples for comparison
609+ rows_cols = []
610+ for cellid in matching_cells :
611+ row = cellid // self .grid .ncol
612+ col = cellid % self .grid .ncol
613+ rows_cols .append ((row , col , cellid ))
614+
615+ # Sort by row first, then column
616+ rows_cols .sort (key = lambda x : (x [0 ], x [1 ]))
617+ return rows_cols [0 ][2 ] # Return the cellid
618+ else :
619+ # For vertex/unstructured grids: choose cell with lowest cell ID
620+ return min (matching_cells )
621+
537622 def _find_layer_for_z (self , icell2d , z ):
538623 """
539624 Find 3D cell index for a 2D cell and z-coordinate.
0 commit comments