From 604b52bcc7102d42fe1cd6c7a31a044adf2d7797 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 8 Mar 2026 19:41:19 +0000 Subject: [PATCH] fix(itinerary): make optimize nearest-neighbor context-aware of anchor positions The optimize function always started nearest-neighbor from the first array element, ignoring where the traveler actually is after preceding anchors (flights, lodging). Now passes the preceding anchor's exit coordinates (destination for transportation) so the algorithm picks the spatially nearest item as the starting point. --- .../CollectionItineraryPlanner.svelte | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/collections/CollectionItineraryPlanner.svelte b/frontend/src/lib/components/collections/CollectionItineraryPlanner.svelte index 6f6e74b3..07c1281f 100644 --- a/frontend/src/lib/components/collections/CollectionItineraryPlanner.svelte +++ b/frontend/src/lib/components/collections/CollectionItineraryPlanner.svelte @@ -1052,14 +1052,35 @@ } function optimizeNearestNeighborSegment( - items: ResolvedItineraryItem[] + items: ResolvedItineraryItem[], + startCoords?: { latitude: number; longitude: number } | null ): ResolvedItineraryItem[] { if (items.length < 2) return [...items]; const remaining = [...items]; const sorted: ResolvedItineraryItem[] = []; - const firstItem = remaining.shift(); + // When we have context coordinates from a preceding anchor (e.g. the + // destination of a flight), pick the nearest item to that position as + // the starting point instead of blindly using the first array element. + let firstItem: ResolvedItineraryItem | undefined; + if (startCoords) { + let nearestIndex = 0; + let nearestDistance = Number.POSITIVE_INFINITY; + for (let i = 0; i < remaining.length; i++) { + const coords = getCoordinatesFromItineraryItem(remaining[i]); + if (!coords) continue; + const distance = haversineDistanceBetweenCoordinates(startCoords, coords); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestIndex = i; + } + } + firstItem = remaining.splice(nearestIndex, 1)[0]; + } else { + firstItem = remaining.shift(); + } + if (!firstItem) return items; sorted.push(firstItem); @@ -1193,7 +1214,24 @@ const optimizedPath: ResolvedItineraryItem[] = []; for (let segmentIndex = 0; segmentIndex < movableSegments.length; segmentIndex += 1) { - const optimizedSegment = optimizeNearestNeighborSegment(movableSegments[segmentIndex]); + // Determine where the traveler will be coming from so the + // nearest-neighbor search starts from a spatially sensible point. + let startCoords: { latitude: number; longitude: number } | null = null; + if (segmentIndex > 0) { + const precedingAnchor = chronologicalAnchors[segmentIndex - 1]; + const anchorType = precedingAnchor?.item?.type || ''; + // For transportation anchors use the *destination* (where the + // traveler arrives), for everything else use the default coords. + startCoords = getCoordinatesFromItineraryItem( + precedingAnchor, + anchorType === 'transportation' ? 'destination' : 'origin' + ); + } + + const optimizedSegment = optimizeNearestNeighborSegment( + movableSegments[segmentIndex], + startCoords + ); optimizedPath.push(...optimizedSegment); if (segmentIndex < chronologicalAnchors.length) {