@@ -462,17 +462,111 @@ function isPolyInPoly(
462462 return false ;
463463 }
464464
465+ const poly1 = getGeom ( feature1 ) ;
465466 const coords = getGeom ( feature2 ) . coordinates ;
466467 for ( const ring of coords ) {
467468 for ( const coord of ring ) {
468469 if ( ! booleanPointInPolygon ( coord , feature1 ) ) {
469470 return false ;
470471 }
471472 }
473+ // Having every vertex inside feature1 is not sufficient when feature1 is concave: an edge of
474+ // feature2 can still cross feature1's exterior (e.g. through a notch) while all its vertices
475+ // remain inside. Split each edge of feature2 on feature1's boundary and reject if any resulting
476+ // sub-segment lies (via its midpoint) strictly in feature1's exterior (#2242).
477+ const segments = splitLineIntoSegmentsOnPolygon (
478+ { type : "LineString" , coordinates : ring } ,
479+ poly1
480+ ) ;
481+ for ( const segment of segments . features ) {
482+ const midpoint = getMidpoint (
483+ segment . geometry . coordinates [ 0 ] ,
484+ segment . geometry . coordinates [ 1 ]
485+ ) ;
486+ if (
487+ ! booleanPointInPolygon ( midpoint , feature1 ) &&
488+ ! isPointOnPolygonBoundary ( midpoint , poly1 )
489+ ) {
490+ return false ;
491+ }
492+ }
472493 }
473494 return true ;
474495}
475496
497+ // Distance under which a point is treated as lying on a polygon boundary,
498+ // expressed in the coordinate units of the input (degrees for WGS84 lng/lat).
499+ // GeoJSON coordinates are only meaningful to about 6 decimal places, roughly
500+ // 10 cm on the ground, per RFC 7946 section 11.2, so anything closer than this
501+ // to an edge is treated as coincident with it. This is a real distance, unlike
502+ // a raw cross-product epsilon which scales with edge length and coordinate
503+ // magnitude and so is not scale invariant.
504+ // https://datatracker.ietf.org/doc/html/rfc7946#section-11.2
505+ const BOUNDARY_DISTANCE_TOLERANCE = 1e-6 ;
506+
507+ /**
508+ * Shortest distance from a point to a line segment on the plane, in the
509+ * coordinate units of the inputs.
510+ *
511+ * @private
512+ * @param {Position } point point [x, y]
513+ * @param {Position } start segment start [x, y]
514+ * @param {Position } end segment end [x, y]
515+ * @returns {number } distance from the point to the segment
516+ */
517+ function pointToSegmentDistance (
518+ point : Position ,
519+ start : Position ,
520+ end : Position
521+ ) : number {
522+ const dx = end [ 0 ] - start [ 0 ] ;
523+ const dy = end [ 1 ] - start [ 1 ] ;
524+ const segmentLengthSquared = dx * dx + dy * dy ;
525+
526+ // Project the point onto the segment, clamping to the endpoints. A
527+ // zero-length segment collapses to its shared endpoint (t stays 0).
528+ let t = 0 ;
529+ if ( segmentLengthSquared > 0 ) {
530+ t =
531+ ( ( point [ 0 ] - start [ 0 ] ) * dx + ( point [ 1 ] - start [ 1 ] ) * dy ) /
532+ segmentLengthSquared ;
533+ t = Math . max ( 0 , Math . min ( 1 , t ) ) ;
534+ }
535+
536+ const closestX = start [ 0 ] + t * dx ;
537+ const closestY = start [ 1 ] + t * dy ;
538+ const offsetX = point [ 0 ] - closestX ;
539+ const offsetY = point [ 1 ] - closestY ;
540+ return Math . sqrt ( offsetX * offsetX + offsetY * offsetY ) ;
541+ }
542+
543+ /**
544+ * Whether a point lies on the boundary of a polygon, within
545+ * BOUNDARY_DISTANCE_TOLERANCE.
546+ * Used to distinguish edges that run along the polygon boundary (allowed for containment) from
547+ * edges that cross into the polygon's exterior. The tolerance absorbs the floating-point
548+ * error introduced when splitting boundary-coincident edges, while staying far smaller than any
549+ * real excursion into the exterior.
550+ *
551+ * @private
552+ * @param {Position } point point [x, y]
553+ * @param {Polygon } polygon polygon geometry
554+ * @returns {boolean } true if the point lies on any ring of the polygon
555+ */
556+ function isPointOnPolygonBoundary ( point : Position , polygon : Polygon ) : boolean {
557+ return polygon . coordinates . some ( ( ring ) => {
558+ for ( let i = 0 ; i < ring . length - 1 ; i ++ ) {
559+ if (
560+ pointToSegmentDistance ( point , ring [ i ] , ring [ i + 1 ] ) <=
561+ BOUNDARY_DISTANCE_TOLERANCE
562+ ) {
563+ return true ;
564+ }
565+ }
566+ return false ;
567+ } ) ;
568+ }
569+
476570function doBBoxOverlap ( bbox1 : BBox , bbox2 : BBox ) {
477571 if ( bbox1 [ 0 ] > bbox2 [ 0 ] ) {
478572 return false ;
0 commit comments