PageSourceSearch

https://www.execucar.com/booking/scripts/googleMaps-dropdown.js?v=Z3d-lEaPLE-O0Kj-IxXRsfqezfk3eMHZy4zH-gDO_tc

js execucar.com collected 2026-09-24 23:41:36 UTC 112,162 bytes, 2,419 lines download raw bytes

1// Enhanced autocomplete with C#-driven dropdown data
2// Material Design 3 - Fixed dropdown positioning
3// Version: v65 - Section header styling (FAVORITES, AIRPORTS pop more)
4
5window.__gmVersion = "AddrEntry v83-BoldPrimaryText";
6console.log("[GOOGLEMAPS-DROPDOWN] loaded", window.__gmVersion);
7
8// ============================================================
9// SS NAMESPACE - Ensure it exists and add nearby location functions
10// ============================================================
11window.ss = window.ss || {};
12
13// Store nearby location from C# reverse geocoding
14window.ss._nearbyLocation = null;
15
16window.ss.setNearbyLocation = function (locationData) {
17    console.log('[SS] setNearbyLocation called:', locationData);
18
19    // Normalize property names: C# sends different names than JS expects
20    if (locationData) {
21        // address → formattedAddress
22        if (locationData.address && !locationData.formattedAddress) {
23            locationData.formattedAddress = locationData.address;
24        }
25        // lat → latitude
26        if (locationData.lat !== undefined && locationData.latitude === undefined) {
27            locationData.latitude = locationData.lat;
28        }
29        // lng → longitude
30        if (locationData.lng !== undefined && locationData.longitude === undefined) {
31            locationData.longitude = locationData.lng;
32        }
33    }
34
35    window.ss._nearbyLocation = locationData;
36    // locationData contains: { address, placeId, lat, lng } (normalized to formattedAddress, latitude, longitude)
37
38    // Dispatch event so any open dropdown can re-render with the new location
39    window.dispatchEvent(new CustomEvent('nearbyLocation:ready', { detail: locationData }));
40};
41
42window.ss.getNearbyLocation = function () {
43    return window.ss._nearbyLocation;
44};
45// ============================================================
46
47// ============================================================
48// KEYBOARD NAVIGATION STATE AND HELPERS
49// Enables Arrow Up/Down and Enter to select from autocomplete dropdown
50// ============================================================
51window.__keyboardHighlightIndex = -1;
52
53/**
54 * Get all selectable dropdown items (excludes section headers, show-more, etc.)
55 */
56window.__getSelectableItems = function (dropdown) {
57    if (!dropdown) {
58        console.log("[GMAPS] __getSelectableItems: dropdown is null");
59        return [];
60    }
61    // Get dropdown-item elements that are NOT show-more links or non-selectable items
62    const items = Array.from(dropdown.querySelectorAll('.dropdown-item:not(.show-more-item)'));
63    console.log("[GMAPS] __getSelectableItems found:", items.length, "items");
64    return items;
65};
66
67/**
68 * Clear keyboard highlight from all items in dropdown
69 */
70window.__clearKeyboardHighlight = function (dropdown) {
71    if (!dropdown) return;
72    dropdown.querySelectorAll('.dropdown-item.keyboard-highlight').forEach(item => {
73        item.classList.remove('keyboard-highlight');
74        // Clear inline styles
75        item.style.backgroundColor = '';
76        item.style.outline = '';
77        item.style.outlineOffset = '';
78    });
79};
80
81/**
82 * Highlight item at given index
83 */
84window.__highlightItemAtIndex = function (dropdown, index, inputEl) {
85    if (!dropdown) {
86        console.log("[GMAPS] __highlightItemAtIndex: dropdown is null");
87        return;
88    }
89
90    window.__clearKeyboardHighlight(dropdown);
91
92    const items = window.__getSelectableItems(dropdown);
93    console.log("[GMAPS] __highlightItemAtIndex:", { index, totalItems: items.length });
94
95    if (index >= 0 && index < items.length) {
96        const item = items[index];
97        item.classList.add('keyboard-highlight');
98
99        // Apply inline styles directly for guaranteed visibility
100        item.style.backgroundColor = '#e8f0fe';
101        item.style.outline = '2px solid #1a73e8';
102        item.style.outlineOffset = '-2px';
103
104        // Set ID for aria-activedescendant
105        const itemId = 'autocomplete-item-' + index;
106        item.id = itemId;
107        item.setAttribute('aria-selected', 'true');
108
109        // Update input's aria-activedescendant for screen reader announcement
110        if (inputEl) {
111            inputEl.setAttribute('aria-activedescendant', itemId);
112        }
113
114        console.log("[GMAPS] Added keyboard-highlight to item:", item.getAttribute('aria-label') || item.textContent?.substring(0, 50));
115
116        // Scroll item into view if needed
117        item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
118    } else if (inputEl) {
119        // Clear aria-activedescendant when no item is highlighted
120        inputEl.removeAttribute('aria-activedescendant');
121    }
122};
123
124/**
125 * Reset keyboard navigation state
126 */
127window.__resetKeyboardNavigation = function (dropdown) {
128    window.__keyboardHighlightIndex = -1;
129    if (dropdown) {
130        window.__clearKeyboardHighlight(dropdown);
131    }
132};
133// ============================================================
134
135// PROGRESSIVE LOCATION STRATEGY:
136// 1. App start: NO location request (no permission prompt)
137// 2. "Book a Reservation" click: Request location (valid user gesture on main page)
138// 3. Modal opens: Use pre-fetched location from C# (Safari iOS doesn't allow new requests in modal)
139// 4. "Use Current Location" click: Use cached location, only request fresh if nothing cached
140
141/**
142 * Initialize autocomplete with pre-prepared dropdown data from C#
143 * @param {HTMLElement} inputEl - The input element
144 * @param {object} dropdownData - Complete dropdown structure from C#
145 * @param {object} allAirportsData - Full airport list for "View all" functionality
146 * @param {object} dotNetRef - .NET object reference for callbacks
147 * @param {string} callbackName - Method name to invoke on selection
148 * @param {object} locationBias - Bias settings for Google Places (includes deviceAccuracy, deviceLatitude, deviceLongitude, mode, referenceLatitude, referenceLongitude)
149 */
150window.initAutocompleteWithDropdown = function (inputEl, dropdownData, allAirportsData, dotNetRef, callbackName, locationBias) {
151    if (!inputEl) {
152        console.error('[GMAPS] initAutocompleteWithDropdown: input element is null');
153        return;
154    }
155
156    console.log("[GMAPS] initAutocompleteWithDropdown v59-SafariIOSLocationFix", {
157        sections: dropdownData?.sections?.length,
158        allAirports: allAirportsData?.length,
159        callback: callbackName,
160        deviceAccuracy: locationBias?.deviceAccuracy,
161        hasDeviceLocation: !!(locationBias?.deviceLatitude && locationBias?.deviceLongitude),
162        mode: locationBias?.mode,
163        hasReferenceLocation: !!(locationBias?.referenceLatitude && locationBias?.referenceLongitude)
164    });
165
166    // If we have device location from C#, store it in global cache for consistency
167    if (locationBias?.deviceLatitude && locationBias?.deviceLongitude) {
168        window.__lastGeo = {
169            lat: locationBias.deviceLatitude,
170            lng: locationBias.deviceLongitude,
171            accuracy: locationBias.deviceAccuracy || 30
172        };
173        console.log("[GMAPS] Stored C# location in global cache:", window.__lastGeo);
174    }
175
176    // Request COARSE location in background for autocomplete bias (silent, no UI)
177    // This happens when modal opens - doesn't block or show loading
178    // NOTE: This may fail on Safari iOS in modal context, but that's OK - we have C# location
179    if (!window.__lastGeo && navigator.geolocation) {
180        console.log("[GMAPS] Requesting coarse location for autocomplete bias (background)...");
181        navigator.geolocation.getCurrentPosition(
182            (position) => {
183                window.__lastGeo = {
184                    lat: position.coords.latitude,
185                    lng: position.coords.longitude,
186                    accuracy: position.coords.accuracy
187                };
188                console.log("[GMAPS] Got coarse location for bias:", {
189                    lat: position.coords.latitude,
190                    lng: position.coords.longitude,
191                    accuracy: position.coords.accuracy + "m"
192                });
193
194                // Dispatch event for other components
195                window.dispatchEvent(new CustomEvent('geo:ready', {
196                    detail: window.__lastGeo
197                }));
198            },
199            (error) => {
200                // Silent fail - not critical, just improves autocomplete
201                console.log("[GMAPS] Coarse location not available (not critical):", error.message);
202            },
203            {
204                enableHighAccuracy: false, // COARSE - WiFi/cell tower (~3-5 seconds)
205                timeout: 10000,
206                maximumAge: 300000 // 5 minutes cache
207            }
208        );
209    }
210
211    // Listen for location updates (in case location arrives after dropdown initializes)
212    const onGeoReady = (event) => {
213        console.log("[GMAPS] Location became available, rebuilding nearby airports");
214
215        const geoData = event.detail || window.__lastGeo;
216        if (!geoData || !geoData.lat || !geoData.lng) {
217            console.warn("[GMAPS] No valid location data in geo:ready event");
218            return;
219        }
220
221        // Check if this input is still focused and has no value
222        if (document.activeElement === inputEl && !inputEl.value.trim()) {
223            console.log("[GMAPS] Rebuilding dropdown with nearby airports based on location");
224
225            // Rebuild dropdown with nearby airports
226            rebuildDropdownWithLocation(geoData.lat, geoData.lng);
227        }
228    };
229
230    // Listen once for geo:ready event
231    window.addEventListener('geo:ready', onGeoReady, { once: true });
232
233    // Listen for nearby location updates (reverse geocoding completed after modal opened)
234    const onNearbyLocationReady = (event) => {
235        // Update our local variables from the global cache (already normalized in setNearbyLocation)
236        nearbyLocation = window.ss._nearbyLocation;
237        hasNearbyLocation = nearbyLocation && nearbyLocation.formattedAddress;
238
239        console.log("[GMAPS] Nearby location became available:", nearbyLocation?.formattedAddress, "hasNearbyLocation:", hasNearbyLocation);
240
241        // Only re-render if input is focused and user hasn't typed anything
242        if (document.activeElement === inputEl && !inputEl.value.trim()) {
243            console.log("[GMAPS] Re-rendering dropdown with 'Your Location'");
244            renderDropdown(originalDropdownData);
245        }
246    };
247
248    // Listen for nearbyLocation:ready event (can fire multiple times if location updates)
249    window.addEventListener('nearbyLocation:ready', onNearbyLocationReady);
250
251    // Store reference for cleanup
252    inputEl.__nearbyLocationHandler = onNearbyLocationReady;
253
254    // Function to rebuild dropdown with nearby airports when location becomes available
255    function rebuildDropdownWithLocation(lat, lng) {
256        if (!originalDropdownData || !originalDropdownData.sections || !allAirportsData) {
257            console.log("[GMAPS] Cannot rebuild - missing data");
258            return;
259        }
260
261        console.log("[GMAPS] Rebuilding with location:", lat, lng);
262
263        // Filter to serviced airports only, then calculate distances
264        const servicedAirports = allAirportsData.filter(airport => isAirportServiced(airport));
265        console.log("[GMAPS] Serviced airports:", servicedAirports.length, "of", allAirportsData.length);
266
267        const airportsWithDistance = servicedAirports.map(airport => {
268            const distance = calculateDistance(lat, lng, airport.latitude, airport.longitude);
269            return { ...airport, distance };
270        });
271
272        // Sort by distance, then by major status
273        airportsWithDistance.sort((a, b) => {
274            // Major airports first
275            const aMajor = a.isMajorAirport || a.IsMajorAirport;
276            const bMajor = b.isMajorAirport || b.IsMajorAirport;
277            if (aMajor && !bMajor) return -1;
278            if (!aMajor && bMajor) return 1;
279
280            // Then by distance
281            return a.distance - b.distance;
282        });
283
284        // Get nearby airports (within ~120 miles / 200km)
285        const nearbyAirports = airportsWithDistance
286            .filter(a => a.distance <= 120)
287            .slice(0, 6);
288
289        // Start with nearby airports
290        let displayAirports = [...nearbyAirports];
291
292        // If fewer than 6 nearby, fill with top-ranked airports
293        if (displayAirports.length < 6) {
294            // Get airports not already in the nearby list, sorted by rank
295            const nearbyIds = new Set(nearbyAirports.map(a => (a.airportID || a.AirportID || '').toUpperCase()));
296
297            let rankedAirports = servicedAirports
298                .filter(a => {
299                    const id = (a.airportID || a.AirportID || '').toUpperCase();
300                    return !nearbyIds.has(id);
301                })
302                .map(a => ({
303                    ...a,
304                    distance: calculateDistance(lat, lng, a.latitude, a.longitude)
305                }));
306
307            // For dropoff mode, limit ranked fill airports to 200 miles
308            if (isDropoffMode) {
309                rankedAirports = rankedAirports.filter(a => a.distance <= 200);
310            }
311
312            rankedAirports = rankedAirports.sort((a, b) => {
313                // Sort by AirportRank (lower is better, 999 is unranked)
314                const rankA = a.airportRank ?? a.AirportRank ?? 999;
315                const rankB = b.airportRank ?? b.AirportRank ?? 999;
316                return rankA - rankB;
317            });
318
319            // Fill up to 6 total
320            const needed = 6 - displayAirports.length;
321            const topRanked = rankedAirports.slice(0, needed);
322            displayAirports = [...displayAirports, ...topRanked];
323
324            console.log("[GMAPS] Filled airports:", displayAirports.length,
325                "(", nearbyAirports.length, "nearby +", topRanked.length, "ranked)",
326                isDropoffMode ? "(dropoff mode - 200mi limit)" : "");
327        }
328
329        // If still no airports (edge case), show top 6 by rank (but respect dropoff distance limit)
330        if (displayAirports.length === 0) {
331            console.log("[GMAPS] No nearby airports, showing top ranked");
332            let fallbackAirports = servicedAirports
333                .map(a => ({
334                    ...a,
335                    distance: calculateDistance(lat, lng, a.latitude, a.longitude)
336                }));
337
338            // For dropoff mode, still limit to 200 miles
339            if (isDropoffMode) {
340                fallbackAirports = fallbackAirports.filter(a => a.distance <= 200);
341            }
342
343            displayAirports = fallbackAirports
344                .sort((a, b) => {
345                    const rankA = a.airportRank ?? a.AirportRank ?? 999;
346                    const rankB = b.airportRank ?? b.AirportRank ?? 999;
347                    return rankA - rankB;
348                })
349                .slice(0, 6);
350        }
351
352        console.log("[GMAPS] Final airport list:", displayAirports.map(a => a.airportID || a.AirportID).join(", "));
353
354        // Create updated dropdown data with airports section + View All link
355        const airportItems = displayAirports.map(airport => ({
356            type: "airport",
357            icon: getPortIcon(airport.portType || airport.PortType, isDropoffMode ? 1 : 0),
358            primaryText: `${airport.airportID || airport.AirportID}`,
359            secondaryText: `${airport.name || airport.Name || ''} - ${airport.distance.toFixed(0)} mi`,
360            data: airport
361        }));
362
363        // Add "View All Airports" link
364        airportItems.push({
365            type: "view_all_airports",
366            icon: "arrow_forward",
367            primaryText: "View all airports",
368            secondaryText: "",
369            isAction: true
370        });
371
372        const updatedData = {
373            sections: [
374                // Keep existing sections (favorites, etc.) but add/update airports
375                ...originalDropdownData.sections.filter(s => s.type !== 'airports'),
376                {
377                    title: "Airports",
378                    type: "airports",
379                    icon: "flight",
380                    items: airportItems
381                }
382            ]
383        };
384
385        // Store updated data as new original
386        originalDropdownData = updatedData;
387
388        // Re-render dropdown
389        renderDropdown(updatedData);
390    }
391
392    // Detect if mobile
393    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768;
394
395    // Get mode (Pickup = 0, Dropoff = 1)
396    const isDropoffMode = locationBias?.mode === 1;
397
398    // Get reference location for distance filtering
399    const referenceLocation = (locationBias?.referenceLatitude && locationBias?.referenceLongitude)
400        ? { lat: locationBias.referenceLatitude, lng: locationBias.referenceLongitude }
401        : null;
402
403    // Check if we already have a precise location from parent component (≤30m accuracy)
404    const hasGoodLocation = locationBias?.deviceLatitude &&
405        locationBias?.deviceLongitude &&
406        locationBias?.deviceAccuracy &&
407        locationBias.deviceAccuracy <= 30;
408
409    // Check if we have a reverse-geocoded nearby location from C#
410    // Use 'let' so we can update from global cache if it arrives later
411    let nearbyLocation = locationBias?.nearbyLocation;
412    let hasNearbyLocation = nearbyLocation && nearbyLocation.formattedAddress;
413
414    // Check if we should hide "Your Location" option (for dropoff when pickup is > 200 miles away)
415    const hideCurrentLocation = locationBias?.hideCurrentLocation === true;
416
417    console.log("[GMAPS] Configuration:", {
418        isMobile,
419        isDropoffMode,
420        suppressAirports: locationBias?.suppressAirports,
421        hasReferenceLocation: !!referenceLocation,
422        hasGoodLocation,
423        deviceAccuracy: locationBias?.deviceAccuracy,
424        hasNearbyLocation,
425        nearbyAddress: nearbyLocation?.formattedAddress,
426        hideCurrentLocation
427    });
428
429    // Additional debug logging
430    console.log("[GMAPS] Location state detail:", {
431        "locationBias.deviceLatitude": locationBias?.deviceLatitude,
432        "locationBias.deviceLongitude": locationBias?.deviceLongitude,
433        "locationBias.deviceAccuracy": locationBias?.deviceAccuracy,
434        "locationBias.nearbyLocation": locationBias?.nearbyLocation ? "exists" : "null",
435        "nearbyLocation.formattedAddress": nearbyLocation?.formattedAddress || "n/a"
436    });
437
438    // Find the .address-field container (the float-field wrapper)
439    // This is the positioning anchor for the dropdown
440    const fieldContainer = inputEl.closest('.address-field') || inputEl.closest('.float-field') || inputEl.parentElement;
441
442    // Ensure the container has relative positioning
443    fieldContainer.style.position = "relative";
444
445    // Create dropdown menu - append to field container for proper positioning
446    const menu = document.createElement("div");
447    menu.className = "autocomplete-dropdown";
448    menu.setAttribute("role", "listbox");
449    menu.setAttribute("aria-label", "Address suggestions");
450
451    // Function to calculate and set dropdown height
452    const setDropdownHeight = () => {
453        const inputRect = inputEl.getBoundingClientRect();
454        const modalContent = inputEl.closest('.address-dialog-content');
455
456        let availableHeight;
457        if (modalContent) {
458            const modalRect = modalContent.getBoundingClientRect();
459            availableHeight = modalRect.bottom - inputRect.bottom - 100;
460            console.log('[GMAPS] Dropdown sizing (modal-based):', {
461                inputBottom: inputRect.bottom,
462                modalBottom: modalRect.bottom,
463                modalHeight: modalRect.height,
464                calculated: availableHeight
465            });
466        } else {
467            availableHeight = window.innerHeight - inputRect.bottom - 150;
468            console.log('[GMAPS] Dropdown sizing (viewport-based):', {
469                viewportHeight: window.innerHeight,
470                inputBottom: inputRect.bottom,
471                calculated: availableHeight
472            });
473        }
474
475        const dropdownMaxHeight = Math.max(350, Math.min(availableHeight, 600));
476        console.log('[GMAPS] Final dropdown height:', dropdownMaxHeight);
477
478        // Update the max-height
479        menu.style.maxHeight = `${dropdownMaxHeight}px`;
480        return dropdownMaxHeight;
481    };
482
483    // Initial calculation and styling
484    const initialHeight = setDropdownHeight();
485
486    // Apply critical positioning styles inline
487    menu.style.cssText = `
488        position: absolute;
489        top: 100%;
490        left: 0;
491        width: 100%;
492        background: white;
493        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
494        border-radius: 0 0 8px 8px;
495        overflow-y: auto;
496        z-index: 10000;
497        max-height: ${initialHeight}px;
498    `;
499
500    // Track if menu is attached
501    if (!window.__acMenus) window.__acMenus = new Set();
502    window.__acMenus.add(menu);
503    fieldContainer.appendChild(menu);
504
505    // Set up ARIA attributes on input for screen reader support
506    // NOTE: On touch devices, skip role="combobox" — iPadOS Safari suppresses the
507    // virtual keyboard for combobox inputs, expecting a native picker instead.
508    const menuId = 'autocomplete-listbox-' + Math.random().toString(36).substr(2, 9);
509    menu.id = menuId;
510    if (navigator.maxTouchPoints === 0) {
511        inputEl.setAttribute('role', 'combobox');
512        inputEl.setAttribute('aria-haspopup', 'listbox');
513    }
514    inputEl.setAttribute('aria-autocomplete', 'list');
515    inputEl.setAttribute('aria-controls', menuId);
516    inputEl.setAttribute('aria-expanded', 'false');
517
518    // Handle window resize
519    let resizeTimeout;
520    window.addEventListener('resize', () => {
521        clearTimeout(resizeTimeout);
522        resizeTimeout = setTimeout(() => {
523            if (menu.parentElement) {
524                setDropdownHeight();
525            }
526        }, 100);
527    });
528
529    // Keep original dropdown data for reset
530    let originalDropdownData = JSON.parse(JSON.stringify(dropdownData || {}));
531
532    // Store state on input element for external updates (e.g., when pickup changes)
533    inputEl.__acState = {
534        dropdownData: originalDropdownData,
535        allAirportsData: allAirportsData,
536        locationBias: locationBias,
537        // Update function to modify closure variables
538        updateData: function(newDropdownData, newAirportsData, newLocationBias) {
539            originalDropdownData = JSON.parse(JSON.stringify(newDropdownData || {}));
540            allAirportsData = newAirportsData;
541            // Update locationBias properties that affect filtering
542            if (newLocationBias) {
543                locationBias = { ...locationBias, ...newLocationBias };
544            }
545            console.log("[GMAPS] Updated autocomplete state:", {
546                sections: originalDropdownData?.sections?.length,
547                airports: allAirportsData?.length,
548                suppressAirports: locationBias?.suppressAirports
549            });
550
551            // If the initial quick-picks dropdown is currently open on an empty input
552            // (e.g., focus landed on Dropoff right as the pickup-based airports were
553            // rebuilt), re-render it now so the user sees the updated nearby airports
554            // immediately instead of stale data. Don't touch it while the user is typing,
555            // so live search results aren't clobbered.
556            try {
557                const isOpen = menu && menu.parentElement && menu.children.length > 0;
558                if (isOpen && inputEl.value.trim().length === 0) {
559                    console.log("[GMAPS] Re-rendering open quick-picks dropdown with updated data");
560                    renderDropdown(originalDropdownData);
561                }
562            } catch (e) {
563                console.warn("[GMAPS] Could not re-render dropdown after update:", e);
564            }
565        }
566    };
567
568    // Google Places Autocomplete Service
569    let autocompleteService = null;
570    if (window.google && google.maps && google.maps.places) {
571        autocompleteService = new google.maps.places.AutocompleteService();
572    }
573
574    // Extract unique country codes from serviced airports for Google Places restriction
575    // Google Places API allows up to 5 countries in componentRestrictions
576    let servicedCountryCodes = [];
577    if (allAirportsData && allAirportsData.length > 0) {
578        const uniqueCountries = new Set();
579        allAirportsData.forEach(airport => {
580            // Only include serviced airports
581            if (isAirportServiced(airport)) {
582                const code = (airport.countryCode || airport.CountryCode || '').toUpperCase();
583                if (code && code.length === 2) {
584                    uniqueCountries.add(code);
585                }
586            }
587        });
588        servicedCountryCodes = Array.from(uniqueCountries).slice(0, 5); // Google allows max 5
589        console.log("[GMAPS] Country restriction from serviced airports:", servicedCountryCodes);
590    }
591
592    // Render dropdown with dynamic data
593    function renderDropdown(data) {
594        // Update aria-expanded when dropdown has content
595        inputEl.setAttribute('aria-expanded', 'true');
596
597        // Fallback: check global cache if we don't have nearby location from init
598        // This handles the case where reverse geocoding completes after modal opens
599        if (!nearbyLocation && window.ss._nearbyLocation) {
600            nearbyLocation = window.ss._nearbyLocation;
601            // Normalize property names (should already be normalized in setNearbyLocation, but be safe)
602            if (nearbyLocation.address && !nearbyLocation.formattedAddress) {
603                nearbyLocation.formattedAddress = nearbyLocation.address;
604            }
605            if (nearbyLocation.lat !== undefined && nearbyLocation.latitude === undefined) {
606                nearbyLocation.latitude = nearbyLocation.lat;
607            }
608            if (nearbyLocation.lng !== undefined && nearbyLocation.longitude === undefined) {
609                nearbyLocation.longitude = nearbyLocation.lng;
610            }
611            hasNearbyLocation = nearbyLocation && nearbyLocation.formattedAddress;
612            console.log("[GMAPS] Picked up nearby location from global cache:", nearbyLocation?.formattedAddress);
613        }
614
615        // Recalculate max-height now that modal is positioned
616        setDropdownHeight();
617        menu.innerHTML = "";
618
619        // Reset keyboard navigation when dropdown content changes
620        window.__resetKeyboardNavigation(menu);
621
622        console.log("[GMAPS] renderDropdown called, sections:", data?.sections?.length);
623
624        // Determine what location option to show at the top
625        const isTyping = inputEl.value.trim().length > 0;
626
627        // Check if we should defer "Your Location" to after airports
628        // (for dropoff mode when pickup was not an airport)
629        const pickupWasNotAirport = !locationBias?.suppressAirports;
630        const deferYourLocation = isDropoffMode && pickupWasNotAirport;
631
632        console.log("[GMAPS] Location option decision:", {
633            isTyping,
634            hasNearbyLocation,
635            hideCurrentLocation,
636            deferYourLocation,
637            willShow: hideCurrentLocation ? "hidden (pickup > 200mi from current location)" :
638                (!isTyping && hasNearbyLocation ? (deferYourLocation ? "Your Location (after airports)" : "Your Location (at top)") : "nothing")
639        });
640
641        // Show "Your Location" at top UNLESS we're deferring it to after airports OR hiding due to distance
642        if (!isTyping && hasNearbyLocation && !deferYourLocation && !hideCurrentLocation) {
643            // We have a reverse-geocoded address - show "Your Location" with the address
644            console.log("[GMAPS] ✓ Showing 'Your Location' item at top:", nearbyLocation?.formattedAddress);
645            const yourLocationItem = createYourLocationItem(nearbyLocation);
646            menu.appendChild(yourLocationItem);
647        }
648
649        // Now check if we have any sections to render
650        if (!data || !data.sections || data.sections.length === 0) {
651            console.log("[GMAPS] No sections to render");
652            return;
653        }
654
655        // Reorder sections based on pickup type:
656        // - If pickup was NOT an airport: put airports first (most trips are to/from airports)
657        // - If pickup WAS an airport: put airports last (airport-to-airport trips are rare)
658        let sectionsToRender = data.sections;
659
660        // Helper to identify airports section (check type OR title)
661        const isAirportsSection = (s) =>
662            s.type === "airports" ||
663            s.type === "Airports" ||
664            (s.title && s.title.toLowerCase() === "airports");
665
666        const pickupWasAirport = !pickupWasNotAirport;
667
668        console.log("[GMAPS] Section reorder check:", {
669            isDropoffMode,
670            suppressAirports: locationBias?.suppressAirports,
671            pickupWasNotAirport,
672            pickupWasAirport,
673            shouldPrioritizeAirports: isDropoffMode && pickupWasNotAirport,
674            shouldDeprioritizeAirports: isDropoffMode && pickupWasAirport,
675            sections: data.sections.map(s => ({ type: s.type, title: s.title, isAirports: isAirportsSection(s) }))
676        });
677
678        // Track if we've rendered "Your Location" after airports
679        let yourLocationRendered = false;
680
681        if (isDropoffMode && pickupWasNotAirport) {
682            console.log("[GMAPS] Dropoff mode with non-airport pickup - prioritizing airports in dropdown");
683            // Sort sections: airports first, then everything else in original order
684            sectionsToRender = [...data.sections].sort((a, b) => {
685                const aIsAirports = isAirportsSection(a);
686                const bIsAirports = isAirportsSection(b);
687                if (aIsAirports && !bIsAirports) return -1;
688                if (!aIsAirports && bIsAirports) return 1;
689                return 0; // Keep original order for non-airport sections
690            });
691            console.log("[GMAPS] Reordered sections:", sectionsToRender.map(s => s.title || s.type));
692        } else if (isDropoffMode && pickupWasAirport) {
693            console.log("[GMAPS] Dropoff mode with airport pickup - moving airports to end of dropdown");
694            // Sort sections: airports last, everything else keeps original order
695            sectionsToRender = [...data.sections].sort((a, b) => {
696                const aIsAirports = isAirportsSection(a);
697                const bIsAirports = isAirportsSection(b);
698                if (aIsAirports && !bIsAirports) return 1;  // airports go to end
699                if (!aIsAirports && bIsAirports) return -1; // non-airports stay before
700                return 0; // Keep original order within same type
701            });
702            console.log("[GMAPS] Reordered sections:", sectionsToRender.map(s => s.title || s.type));
703        }
704
705        // Show "Quick Picks" banner when user hasn't typed anything yet
706        if (!isTyping && sectionsToRender.length > 0) {
707            const quickPicksBanner = document.createElement("div");
708            quickPicksBanner.className = "quick-picks-banner";
709            quickPicksBanner.style.cssText = `
710                padding: 8px 14px 6px;
711                font-size: 11px;
712                border-bottom: 1px solid #e2e8f0;
713                display: flex;
714                align-items: center;
715                justify-content: flex-start;
716                gap: 5px;
717            `;
718            const label = document.createElement("span");
719            label.style.cssText = "font-weight: 500; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;";
720            label.textContent = "Quick picks";
721            const hint = document.createElement("span");
722            hint.style.cssText = "font-weight: 400; color: #b0b7c3; font-size: 11px;";
723            hint.textContent = "— or type any address above";
724            quickPicksBanner.appendChild(label);
725            quickPicksBanner.appendChild(hint);
726            menu.appendChild(quickPicksBanner);
727        }
728
729        sectionsToRender.forEach((section, index) => {
730            // Skip empty sections
731            if (!section.items || section.items.length === 0) return;
732
733            // Skip "nearby" section - JS already shows this as "Your Location" item
734            // This prevents duplicate location entries in the dropdown
735            if (section.type === "nearby" || section.type === "nearby_location" ||
736                (section.title && section.title.toLowerCase().includes("nearby"))) {
737                console.log("[GMAPS] Skipping nearby section - already shown as 'Your Location' item");
738                return;
739            }
740
741            // Filter items based on distance for dropoff mode
742            let filteredItems = section.items;
743
744            // For dropoff mode, filter favorites/nearby if > 200 miles from reference location
745            if (isDropoffMode && referenceLocation && (section.type === "favorites" || section.type === "nearby")) {
746                filteredItems = section.items.filter(item => {
747                    // Get item location
748                    const itemLat = item.data?.latitude || item.data?.Latitude;
749                    const itemLng = item.data?.longitude || item.data?.Longitude;
750
751                    if (itemLat && itemLng) {
752                        const distance = calculateDistance(referenceLocation.lat, referenceLocation.lng, itemLat, itemLng);
753                        console.log(`[GMAPS] Distance check for ${item.primaryText}: ${distance.toFixed(1)} miles`);
754                        return distance <= 200;
755                    }
756
757                    // If no coordinates, include it (let user decide)
758                    return true;
759                });
760
761                console.log(`[GMAPS] Dropoff filtering: ${section.items.length} → ${filteredItems.length} items (within 200 miles)`);
762            }
763
764            // Skip if all items filtered out
765            if (filteredItems.length === 0) return;
766
767            const sectionDiv = document.createElement("div");
768            sectionDiv.className = "dropdown-section";
769            sectionDiv.style.cssText = "border-bottom: 1px solid #f1f3f4;";
770
771            // Add grid layout for airports and favorites
772            if (section.type === "airports") {
773                sectionDiv.classList.add("airports-grid");
774            } else if (section.type === "favorites") {
775                sectionDiv.classList.add("favorites-grid");
776            } else if (section.type === "landmarks") {
777                sectionDiv.classList.add("landmarks-grid");
778            }
779
780            // Section header (if title exists) - prominent styling
781            if (section.title) {
782                const header = document.createElement("div");
783                header.className = "section-header";
784                header.style.cssText = `
785                    padding: 10px 14px 6px;
786                    font-size: 11px;
787                    font-weight: 700;
788                    color: #102B5B;
789                    text-transform: uppercase;
790                    letter-spacing: 0.8px;
791                    background: linear-gradient(to bottom, #e8eef5, #f4f6f9);
792                    border-left: 3px solid #2963A8;
793                    display: flex;
794                    align-items: center;
795                    gap: 6px;
796                `;
797
798                // Add icon based on section type
799                if (section.icon) {
800                    const icon = document.createElement("span");
801                    icon.className = "material-icons";
802                    icon.textContent = getMaterialIcon(section.icon);
803                    icon.style.cssText = "font-size: 16px; color: #2963A8;";
804                    header.appendChild(icon);
805                }
806
807                const titleSpan = document.createElement("span");
808                titleSpan.textContent = section.title;
809                header.appendChild(titleSpan);
810
811                sectionDiv.appendChild(header);
812            }
813
814            // Items container
815            const itemsContainer = document.createElement("div");
816            itemsContainer.className = "dropdown-items";
817
818            // Apply layout based on section type
819            if (section.type === "airports") {
820                // Single column layout for airports (to show city, state)
821                itemsContainer.style.cssText = `
822                    display: flex;
823                    flex-direction: column;
824                    gap: 0;
825                    padding: 0;
826                `;
827            } else if (section.type === "favorites") {
828                // Horizontal flex layout for favorites (single row) - compact
829                itemsContainer.style.cssText = `
830                    display: flex;
831                    flex-wrap: nowrap;
832                    overflow-x: auto;
833                    gap: 4px;
834                    padding: 4px;
835                `;
836            } else if (section.type === "landmarks") {
837                // Vertical list layout for landmarks (hotels) - show full address
838                itemsContainer.style.cssText = `
839                    display: flex;
840                    flex-direction: column;
841                    gap: 0;
842                    padding: 0;
843                `;
844            }
845
846            // Determine if we should limit favorites display
847            const isFavoritesSection = section.type === "favorites" ||
848                (section.title && section.title.toLowerCase().includes("favorite"));
849            const shouldLimitFavorites = isFavoritesSection && filteredItems.length > 2;
850
851            // Show first 2 favorites, or all if not limiting
852            const itemsToShow = shouldLimitFavorites ? filteredItems.slice(0, 2) : filteredItems;
853            const hiddenItemsCount = shouldLimitFavorites ? filteredItems.length - 2 : 0;
854
855            // Render visible items
856            itemsToShow.forEach(item => {
857                const itemEl = createDropdownItem(item);
858                itemsContainer.appendChild(itemEl);
859            });
860
861            // Add "Show More Favorites" link if needed
862            if (shouldLimitFavorites) {
863                const showMoreItem = createShowMoreFavoritesItem(hiddenItemsCount, () => {
864                    // Clear and show all favorites
865                    itemsContainer.innerHTML = "";
866                    filteredItems.forEach(item => {
867                        const itemEl = createDropdownItem(item);
868                        itemsContainer.appendChild(itemEl);
869                    });
870                });
871                itemsContainer.appendChild(showMoreItem);
872            }
873
874            sectionDiv.appendChild(itemsContainer);
875            menu.appendChild(sectionDiv);
876
877            // Insert "Your Location" after airports section when deferred (and not hidden due to distance)
878            if (deferYourLocation && !yourLocationRendered && !isTyping && hasNearbyLocation && !hideCurrentLocation && isAirportsSection(section)) {
879                console.log("[GMAPS] ✓ Inserting 'Your Location' after airports section");
880                const yourLocationItem = createYourLocationItem(nearbyLocation);
881                menu.appendChild(yourLocationItem);
882                yourLocationRendered = true;
883            }
884        });
885
886        console.log("[GMAPS] Dropdown rendered");
887    }
888
889    // Calculate distance between two points in miles using Haversine formula
890    function calculateDistance(lat1, lon1, lat2, lon2) {
891        const R = 3959; // Earth's radius in miles
892        const dLat = (lat2 - lat1) * Math.PI / 180;
893        const dLon = (lon2 - lon1) * Math.PI / 180;
894        const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
895            Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
896            Math.sin(dLon / 2) * Math.sin(dLon / 2);
897        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
898        return R * c;
899    }
900
901    // Get Material Icon name
902    function getMaterialIcon(iconName) {
903        const iconMap = {
904            'star': 'star',
905            'favorite': 'favorite',
906            'place': 'place',
907            'location_on': 'location_on',
908            'flight_land': 'flight_land',
909            'flight_takeoff': 'flight_takeoff',
910            'flight': 'flight',
911            'local_airport': 'flight',
912            'arrow_forward': 'arrow_forward',
913            'link': 'arrow_forward',
914            'near_me': 'near_me',
915            'my_location': 'my_location',
916            'search': 'search',
917            'history': 'history',
918            'directions_boat': 'directions_boat',
919            'train': 'train',
920            'hotel': 'hotel',
921            'business': 'business',
922            'event': 'event',
923            'restaurant': 'restaurant'
924        };
925        return iconMap[iconName] || iconName || 'place';
926    }
927
928    // Get icon based on port type (0=airport, 1=cruise terminal, 2=train station)
929    // For airports, uses mode to determine direction (pickup=landing, dropoff=takeoff)
930    function getPortIcon(portType, mode) {
931        const type = parseInt(portType, 10);
932        switch (type) {
933            case 1: return 'directions_boat';  // Cruise terminal
934            case 2: return 'train';            // Train station
935            case 0:                            // Airport
936            default:
937                // mode 0 = pickup (landing), mode 1 = dropoff (takeoff)
938                return mode === 1 ? 'flight_takeoff' : 'flight_land';
939        }
940    }
941
942    // Check if airport/port is serviced
943    function isAirportServiced(airport) {
944        if (!airport) {
945            console.log("[GMAPS] isAirportServiced: airport is null/undefined");
946            return false;
947        }
948
949        // Use serviced property (single source of truth)
950        const serviced = airport.serviced ?? airport.Serviced;
951        const airportID = airport.airportID || airport.AirportID;
952
953        //console.log("[GMAPS] isAirportServiced:", airportID, "serviced=", serviced, "type=", typeof serviced);
954
955        // Handle boolean, string, or number values
956        if (typeof serviced === 'boolean') {
957            return serviced === true;
958        } else if (typeof serviced === 'string') {
959            return serviced.toLowerCase() === 'true' || serviced === '1';
960        } else if (typeof serviced === 'number') {
961            return serviced === 1;
962        }
963
964        //console.log("[GMAPS] isAirportServiced:", airportID, "- serviced not found, assuming false");
965        return false;
966    }
967
968    // Helper function to build full address from data object
969    function buildFullAddress(data) {
970        if (!data) return null;
971
972        console.log("[GMAPS] buildFullAddress - input data:", JSON.stringify(data).substring(0, 500));
973
974        const parts = [];
975
976        // Street address (try multiple property names from different models)
977        const street = data.streetAddress || data.StreetAddress || data.street_address ||
978            data.address || data.Address || data.formatted_address || data.FormattedAddress;
979        if (street) parts.push(street);
980
981        // City (MemberLocationModel uses "City", PlaceDetailsModel uses "city")
982        const city = data.city || data.City;
983        if (city) parts.push(city);
984
985        // State
986        const state = data.state || data.State;
987        if (state) parts.push(state);
988
989        // Zip (MemberLocationModel uses "Zip", PlaceDetailsModel uses "zipCode" or "ZipCode")
990        const zip = data.zip || data.Zip || data.zipCode || data.ZipCode || data.postal_code;
991        if (zip) parts.push(zip);
992
993        const result = parts.length > 1 ? parts.join(', ') : null; // Only return if we have more than just street
994        console.log("[GMAPS] buildFullAddress - result:", result);
995        return result;
996    }
997
998    // Create "Current Location" item (always shown as first item, compact single-line)
999    function createCurrentLocationItem() {
1000        const itemDiv = document.createElement("div");
1001        itemDiv.className = "dropdown-item current-location-item";
1002        itemDiv.style.cssText = `
1003            padding: 6px 14px;
1004            cursor: pointer;
1005            display: flex;
1006            align-items: center;
1007            gap: 8px;
1008            transition: background-color 0.1s ease;
1009            border-bottom: 1px solid #f1f3f4;
1010            min-height: 32px;
1011            background-color: #e8f0fe;
1012        `;
1013
1014        // Hover effect
1015        itemDiv.addEventListener("mouseenter", () => {
1016            itemDiv.style.backgroundColor = "#d3e3fd";
1017        });
1018        itemDiv.addEventListener("mouseleave", () => {
1019            itemDiv.style.backgroundColor = "#e8f0fe";
1020        });
1021
1022        // Icon
1023        const icon = document.createElement("span");
1024        icon.className = "material-icons";
1025        icon.textContent = "my_location";
1026        icon.style.cssText = "font-size: 18px; color: #1a73e8; flex-shrink: 0;";
1027        icon.setAttribute("aria-hidden", "true");
1028        itemDiv.appendChild(icon);
1029
1030        // Text
1031        const text = document.createElement("div");
1032        text.textContent = "Use Current Location";
1033        text.style.cssText = "font-size: 13px; font-weight: 500; color: #1a73e8;";
1034        itemDiv.appendChild(text);
1035
1036        // Accessibility
1037        itemDiv.setAttribute("role", "option");
1038        itemDiv.setAttribute("aria-label", "Use Current Location");
1039
1040        // Click handler - use pre-fetched location or request if not available
1041        itemDiv.addEventListener("mousedown", (e) => e.preventDefault());
1042        itemDiv.addEventListener("click", async () => {
1043            console.log("[GMAPS] Current Location clicked");
1044
1045            // Platform detection for custom messaging
1046            const isDesktop = !isMobile;
1047            const loadingMessage = isDesktop
1048                ? "Getting your location... (may take 10-15 seconds on desktop)"
1049                : "Getting your location...";
1050
1051            // Show loading state
1052            icon.textContent = "hourglass_empty";
1053            text.textContent = loadingMessage;
1054            itemDiv.style.cursor = "wait";
1055
1056            try {
1057                let lat, lng, accuracy;
1058
1059                // FIRST: Check if we already have location from C# (passed via locationBias)
1060                // This is the location obtained when user clicked "Book a Reservation"
1061                // This is critical for Safari iOS which won't allow new geolocation requests in modal
1062                if (locationBias?.deviceLatitude && locationBias?.deviceLongitude) {
1063                    console.log("[GMAPS] Using pre-fetched location from C#:", locationBias.deviceLatitude, locationBias.deviceLongitude, "accuracy:", locationBias.deviceAccuracy);
1064                    lat = locationBias.deviceLatitude;
1065                    lng = locationBias.deviceLongitude;
1066                    accuracy = locationBias.deviceAccuracy || 30; // Default to acceptable accuracy if not provided
1067                }
1068                // SECOND: Check global cache from app-init or earlier requests
1069                else if (window.__lastGeo?.lat && window.__lastGeo?.lng) {
1070                    console.log("[GMAPS] Using cached location from window.__lastGeo:", window.__lastGeo);
1071                    lat = window.__lastGeo.lat;
1072                    lng = window.__lastGeo.lng;
1073                    accuracy = window.__lastGeo.accuracy || 30;
1074                }
1075                // THIRD: Only request NEW location if we have nothing cached
1076                // NOTE: This will likely FAIL on Safari iOS in modal context, but we try anyway
1077                else {
1078                    console.log("[GMAPS] No cached location - requesting fresh (may fail on Safari iOS in modal)");
1079
1080                    if (!navigator.geolocation) {
1081                        await dotNetRef.invokeMethodAsync('OnLocationError', 'Geolocation is not supported by your browser');
1082                        resetButtonState();
1083                        return;
1084                    }
1085
1086                    const position = await new Promise((resolve, reject) => {
1087                        navigator.geolocation.getCurrentPosition(resolve, reject, {
1088                            enableHighAccuracy: true,
1089                            timeout: 20000,
1090                            maximumAge: 600000 // 10 minutes - user isn't moving while booking
1091                        });
1092                    });
1093
1094                    lat = position.coords.latitude;
1095                    lng = position.coords.longitude;
1096                    accuracy = position.coords.accuracy;
1097                    console.log("[GMAPS] Got fresh location:", { lat, lng, accuracy });
1098                }
1099
1100                console.log("[GMAPS] Using location:", { lat, lng, accuracy });
1101
1102                // Check if accuracy is good enough for reverse geocoding (≤30m threshold)
1103                if (accuracy > 30) {
1104                    console.warn(`[GMAPS] Accuracy too low for address (${accuracy}m > 30m threshold)`);
1105
1106                    // Show error via C# callback
1107                    await dotNetRef.invokeMethodAsync(
1108                        'OnLocationError',
1109                        `We couldn't pinpoint your exact location (accuracy: ${Math.round(accuracy)}m). Please enter your address manually or try again in a location with better GPS signal.`
1110                    );
1111
1112                    resetButtonState();
1113                    return;
1114                }
1115
1116                // Accuracy is good enough (≤30m) - proceed with reverse geocoding
1117                text.textContent = "Finding your address...";
1118
1119                // Reverse geocode using existing function from googleMaps.js
1120                const reverseGeocodeResult = await window.ss.reverseGeocode(lat, lng);
1121
1122                console.log("[GMAPS] Reverse geocoded address:", reverseGeocodeResult);
1123
1124                // Convert to our format
1125                const addressData = {
1126                    formattedAddress: reverseGeocodeResult.address || reverseGeocodeResult.line1,
1127                    streetAddress: reverseGeocodeResult.line1 || '',
1128                    city: reverseGeocodeResult.city || '',
1129                    state: reverseGeocodeResult.state || '',
1130                    zipCode: reverseGeocodeResult.postal || '',
1131                    country: reverseGeocodeResult.country || 'US',
1132                    placeId: reverseGeocodeResult.placeId || '',
1133                    latitude: lat,
1134                    longitude: lng
1135                };
1136
1137                // Create location data with full address details
1138                const locationData = {
1139                    type: "current_location_geocoded",
1140                    latitude: lat,
1141                    longitude: lng,
1142                    accuracy: accuracy,
1143                    formattedAddress: addressData.formattedAddress,
1144                    streetAddress: addressData.streetAddress,
1145                    city: addressData.city,
1146                    state: addressData.state,
1147                    zipCode: addressData.zipCode,
1148                    country: addressData.country,
1149                    placeId: addressData.placeId,
1150                    types: ['street_address'] // Default type for geocoded locations
1151                };
1152
1153                // Call back to C# with the geocoded location
1154                await dotNetRef.invokeMethodAsync(callbackName, locationData);
1155
1156                // Success state
1157                icon.textContent = "check_circle";
1158                text.textContent = "Location set!";
1159                itemDiv.style.backgroundColor = "#e6f4ea";
1160
1161                // Close dropdown after brief delay
1162                setTimeout(() => {
1163                    closeDropdown(true); // true = was a selection
1164                }, 500);
1165
1166            } catch (error) {
1167                console.error("[GMAPS] Location error:", error);
1168
1169                // Determine error type and message
1170                let errorMessage = "Unable to get your location";
1171
1172                if (error.code === 1) { // PERMISSION_DENIED
1173                    errorMessage = "Location access denied. Please enable location permissions in your browser settings.";
1174                } else if (error.code === 2) { // POSITION_UNAVAILABLE
1175                    errorMessage = "Location unavailable. Please check your device settings or try again from the main booking screen.";
1176                } else if (error.code === 3) { // TIMEOUT
1177                    errorMessage = "Location request timed out. Please try again or enter your address manually.";
1178                } else if (error.message && error.message.includes("Geocoding")) {
1179                    errorMessage = "Unable to determine your address. Please enter it manually.";
1180                }
1181
1182                // Show error state
1183                icon.textContent = "location_off";
1184                text.textContent = "Location failed";
1185                itemDiv.style.backgroundColor = "#fce8e6";
1186
1187                // Send error to C# for validation display
1188                await dotNetRef.invokeMethodAsync('OnLocationError', errorMessage);
1189
1190                // Reset after delay
1191                setTimeout(resetButtonState, 3000);
1192            }
1193
1194            function resetButtonState() {
1195                icon.textContent = "my_location";
1196                text.textContent = "Use Current Location";
1197                itemDiv.style.backgroundColor = "#e8f0fe";
1198                itemDiv.style.cursor = "pointer";
1199            }
1200        });
1201
1202        return itemDiv;
1203    }
1204
1205    // Create "Your Location" item when we have a reverse-geocoded address
1206    function createYourLocationItem(location) {
1207        const itemDiv = document.createElement("div");
1208        itemDiv.className = "dropdown-item your-location-item";
1209        itemDiv.style.cssText = `
1210            padding: 8px 14px;
1211            cursor: pointer;
1212            display: flex;
1213            align-items: flex-start;
1214            gap: 10px;
1215            transition: background-color 0.1s ease;
1216            border-bottom: 1px solid #f1f3f4;
1217            min-height: 44px;
1218            background-color: #e8f0fe;
1219        `;
1220
1221        // Hover effect
1222        itemDiv.addEventListener("mouseenter", () => {
1223            itemDiv.style.backgroundColor = "#d3e3fd";
1224        });
1225        itemDiv.addEventListener("mouseleave", () => {
1226            itemDiv.style.backgroundColor = "#e8f0fe";
1227        });
1228
1229        // Icon
1230        const icon = document.createElement("span");
1231        icon.className = "material-icons";
1232        icon.textContent = "my_location";
1233        icon.style.cssText = "font-size: 20px; color: #1a73e8; flex-shrink: 0; margin-top: 2px;";
1234        icon.setAttribute("aria-hidden", "true");
1235        itemDiv.appendChild(icon);
1236
1237        // Text container
1238        const textContainer = document.createElement("div");
1239        textContainer.style.cssText = "flex: 1; min-width: 0;";
1240
1241        // Label
1242        const label = document.createElement("div");
1243        label.textContent = "Your Location";
1244        label.style.cssText = "font-size: 13px; font-weight: 600; color: #1a73e8; margin-bottom: 2px;";
1245        textContainer.appendChild(label);
1246
1247        // Address
1248        const addressText = location.formattedAddress || location.streetAddress || "Current location";
1249        const address = document.createElement("div");
1250        address.textContent = addressText;
1251        address.style.cssText = "font-size: 12px; color: #5f6368; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;";
1252        textContainer.appendChild(address);
1253
1254        itemDiv.appendChild(textContainer);
1255
1256        // Accessibility
1257        itemDiv.setAttribute("role", "option");
1258        itemDiv.setAttribute("aria-label", "Your Location: " + addressText);
1259
1260        // Click handler - use the pre-geocoded location directly
1261        itemDiv.addEventListener("mousedown", (e) => e.preventDefault());
1262        itemDiv.addEventListener("click", async () => {
1263            console.log("[GMAPS] Your Location clicked - using pre-geocoded address:", location);
1264
1265            // Show selection feedback
1266            icon.textContent = "check_circle";
1267            label.textContent = "Location selected!";
1268            itemDiv.style.backgroundColor = "#e6f4ea";
1269
1270            // Build the location data for C#
1271            const locationData = {
1272                type: "current_location_geocoded",
1273                latitude: location.latitude,
1274                longitude: location.longitude,
1275                accuracy: locationBias?.deviceAccuracy || 30,
1276                formattedAddress: location.formattedAddress,
1277                streetAddress: location.streetAddress || '',
1278                city: location.city || '',
1279                state: location.state || '',
1280                zipCode: location.zipCode || '',
1281                country: location.country || 'US',
1282                placeId: location.placeId || '',
1283                types: ['street_address']
1284            };
1285
1286            // Call back to C# with the location
1287            await dotNetRef.invokeMethodAsync(callbackName, locationData);
1288
1289            // Close dropdown after brief delay
1290            setTimeout(() => {
1291                closeDropdown(true); // true = was a selection
1292            }, 300);
1293        });
1294
1295        return itemDiv;
1296    }
1297
1298    // Create "Show More Favorites" link (compact)
1299    function createShowMoreFavoritesItem(count, onClick) {
1300        const itemDiv = document.createElement("div");
1301        itemDiv.className = "dropdown-item show-more-item";
1302        itemDiv.style.cssText = `
1303            padding: 4px 14px;
1304            cursor: pointer;
1305            display: flex;
1306            align-items: center;
1307            justify-content: center;
1308            gap: 4px;
1309            transition: background-color 0.1s ease;
1310            min-height: 28px;
1311            font-size: 12px;
1312            color: #1a73e8;
1313            font-weight: 500;
1314        `;
1315
1316        itemDiv.addEventListener("mouseenter", () => {
1317            itemDiv.style.backgroundColor = "#f1f3f4";
1318        });
1319        itemDiv.addEventListener("mouseleave", () => {
1320            itemDiv.style.backgroundColor = "transparent";
1321        });
1322
1323        const iconEl = document.createElement("span");
1324        iconEl.className = "material-icons";
1325        iconEl.textContent = "expand_more";
1326        iconEl.style.cssText = "font-size: 16px;";
1327        itemDiv.appendChild(iconEl);
1328
1329        const textEl = document.createElement("span");
1330        textEl.textContent = `+${count} more favorites`;
1331        itemDiv.appendChild(textEl);
1332
1333        itemDiv.addEventListener("mousedown", (e) => e.preventDefault());
1334        itemDiv.addEventListener("click", onClick);
1335
1336        return itemDiv;
1337    }
1338
1339    // Create "Show More Favorites" link (compact)
1340    function createDropdownItem(item) {
1341        const itemDiv = document.createElement("div");
1342        itemDiv.className = "dropdown-item";
1343
1344        // Base styling - single line, compact
1345        itemDiv.style.cssText = `
1346            padding: 6px 14px;
1347            cursor: pointer;
1348            display: flex;
1349            align-items: center;
1350            gap: 8px;
1351            transition: background-color 0.1s ease;
1352            min-height: 32px;
1353        `;
1354
1355        // Special sizing for favorites in horizontal layout
1356        if (item.type === "favorite") {
1357            itemDiv.style.cssText = `
1358                padding: 6px 12px;
1359                cursor: pointer;
1360                display: flex;
1361                align-items: center;
1362                gap: 8px;
1363                transition: background-color 0.1s ease;
1364                min-height: 32px;
1365                flex: 0 0 auto;
1366                min-width: 140px;
1367            `;
1368        }
1369
1370        // Compact two-line layout for Google Places (hotels, businesses, etc.)
1371        if (item.type === "google_place") {
1372            itemDiv.style.cssText = `
1373                padding: 4px 14px;
1374                cursor: pointer;
1375                display: flex;
1376                align-items: center;
1377                gap: 8px;
1378                transition: background-color 0.1s ease;
1379                min-height: 38px;
1380            `;
1381            itemDiv.classList.add("google-place-item");
1382        }
1383
1384        // Special styling for action links (like "View All Airports")
1385        if (item.type === "view_all_airports" || item.isAction) {
1386            itemDiv.style.cssText = `
1387                padding: 6px 14px;
1388                cursor: pointer;
1389                display: flex;
1390                align-items: center;
1391                gap: 8px;
1392                transition: background-color 0.1s ease;
1393                min-height: 32px;
1394                background-color: #f8f9fa;
1395                border-top: 1px solid #e8eaed;
1396                grid-column: 1 / -1;
1397            `;
1398        }
1399
1400        // Hover effect
1401        itemDiv.addEventListener("mouseenter", () => {
1402            itemDiv.style.backgroundColor = "#f1f3f4";
1403        });
1404        itemDiv.addEventListener("mouseleave", () => {
1405            if (item.type === "view_all_airports" || item.isAction) {
1406                itemDiv.style.backgroundColor = "#f8f9fa";
1407            } else {
1408                itemDiv.style.backgroundColor = "transparent";
1409            }
1410        });
1411
1412        // Icon - smaller for compact layout
1413        if (item.icon) {
1414            const iconEl = document.createElement("span");
1415            iconEl.className = "material-icons";
1416            iconEl.textContent = getMaterialIcon(item.icon);
1417            iconEl.style.cssText = "font-size: 18px; color: #5f6368; flex-shrink: 0;";
1418            iconEl.setAttribute("aria-hidden", "true"); // Hide icon from screen readers
1419
1420            // Blue icon for action links
1421            if (item.type === "view_all_airports" || item.isAction) {
1422                iconEl.style.color = "#1a73e8";
1423            }
1424
1425            itemDiv.appendChild(iconEl);
1426        }
1427
1428        // Build accessible label for screen readers
1429        let accessibleLabel = item.primaryText || "";
1430        if (item.secondaryText) {
1431            accessibleLabel += ", " + item.secondaryText;
1432        }
1433        itemDiv.setAttribute("role", "option");
1434        itemDiv.setAttribute("aria-label", accessibleLabel);
1435
1436        // Text container - single line with primary and secondary inline
1437        const textContainer = document.createElement("div");
1438        textContainer.style.cssText = "flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
1439
1440        // Build single-line content based on item type
1441        if (item.type === "airport" && item.primaryText) {
1442            // Airport/Train/Cruise: "PHX - Phoenix Sky Harbor" or "BB1 - Boston South Train Station"
1443            // Match codes that can contain letters and numbers (2-4 characters)
1444            const match = item.primaryText.match(/^([A-Z0-9]{2,4})\s*-?\s*(.*)$/i);
1445            if (match) {
1446                const airportCode = match[1].toUpperCase();
1447                const airportName = match[2] || "";
1448
1449                // Bold airport code
1450                const codeSpan = document.createElement("strong");
1451                codeSpan.textContent = airportCode;
1452                codeSpan.style.cssText = "font-weight: 700; color: #202124; font-size: 14px;";
1453                textContainer.appendChild(codeSpan);
1454
1455                // Add name if present
1456                if (airportName) {
1457                    const nameSpan = document.createElement("span");
1458                    nameSpan.textContent = " " + airportName;
1459                    nameSpan.style.cssText = "font-weight: 400; color: #5f6368; font-size: 13px;";
1460                    textContainer.appendChild(nameSpan);
1461                }
1462
1463                // Add secondary (distance) if present
1464                if (item.secondaryText) {
1465                    const secondarySpan = document.createElement("span");
1466                    secondarySpan.textContent = " · " + item.secondaryText;
1467                    secondarySpan.style.cssText = "color: #80868b; font-size: 12px;";
1468                    textContainer.appendChild(secondarySpan);
1469                }
1470            } else {
1471                textContainer.textContent = item.primaryText || "";
1472            }
1473        } else if (item.type === "view_all_airports" || item.isAction) {
1474            // Action link styling
1475            const actionText = document.createElement("span");
1476            actionText.style.cssText = "font-size: 13px; color: #1a73e8; font-weight: 500;";
1477            actionText.textContent = item.primaryText || "";
1478            textContainer.appendChild(actionText);
1479        } else if (item.type === "favorite") {
1480            // Favorite: "Name · Address"
1481            const nameSpan = document.createElement("span");
1482            nameSpan.textContent = item.primaryText || "";
1483            nameSpan.style.cssText = "font-weight: 600; color: #202124; font-size: 13px;";
1484            textContainer.appendChild(nameSpan);
1485
1486            if (item.secondaryText) {
1487                const addrSpan = document.createElement("span");
1488                addrSpan.textContent = " · " + item.secondaryText;
1489                addrSpan.style.cssText = "color: #5f6368; font-size: 12px;";
1490                textContainer.appendChild(addrSpan);
1491            }
1492        } else if (item.type === "google_place") {
1493            // Google place: Two-line compact layout
1494            // Name on first line (bold), address on second line
1495            textContainer.style.cssText = "flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; justify-content: center;";
1496
1497            const nameSpan = document.createElement("div");
1498            nameSpan.textContent = item.primaryText || "";
1499            nameSpan.style.cssText = "font-weight: 600; color: #202124; font-size: 13px; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
1500            textContainer.appendChild(nameSpan);
1501
1502            if (item.secondaryText) {
1503                const addrSpan = document.createElement("div");
1504                addrSpan.textContent = item.secondaryText;
1505                addrSpan.style.cssText = "color: #5f6368; font-size: 11px; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
1506                textContainer.appendChild(addrSpan);
1507            }
1508        } else {
1509            // Default: primary text (semi-bold), then secondary if exists
1510            const primarySpan = document.createElement("span");
1511            primarySpan.textContent = item.primaryText || "";
1512            primarySpan.style.cssText = "font-weight: 600; color: #202124; font-size: 14px;";
1513            textContainer.appendChild(primarySpan);
1514
1515            if (item.secondaryText) {
1516                const secondarySpan = document.createElement("span");
1517                secondarySpan.textContent = " · " + item.secondaryText;
1518                secondarySpan.style.cssText = "color: #5f6368; font-size: 12px;";
1519                textContainer.appendChild(secondarySpan);
1520            }
1521        }
1522
1523        itemDiv.appendChild(textContainer);
1524
1525        // Click handler
1526        itemDiv.addEventListener("mousedown", (e) => e.preventDefault());
1527        itemDiv.addEventListener("click", async () => {
1528            console.log("[GMAPS] Dropdown item clicked:", item);
1529
1530            // Handle based on item type
1531            if (item.type === "google_place") {
1532                // Fetch place details
1533                await fetchPlaceDetails(item.data.placeId);
1534            } else if (item.type === "view_all_airports" || item.data === "show_all_airports") {
1535                // Show all airports - handle both type check and data check
1536                showAllAirports();
1537            } else if (item.type === "airport" && item.data) {
1538                // Check if airport is serviced
1539                if (isAirportServiced(item.data)) {
1540                    // Serviced airport - close dropdown first for immediate feedback, then process
1541                    closeDropdown(true); // true = was a selection
1542                    await dotNetRef.invokeMethodAsync(callbackName, item.data);
1543                } else {
1544                    // Non-serviced airport - call different callback to show error modal
1545                    console.log("[GMAPS] Non-serviced airport selected:", item.data.airportID || item.data.AirportID);
1546                    await dotNetRef.invokeMethodAsync("OnNonServicedAirportSelected", item.data);
1547                    closeDropdown(); // false - not a valid selection, user needs to pick again
1548                }
1549            } else {
1550                // Direct callback with data (favorites, etc.) - close dropdown first for immediate feedback
1551                closeDropdown(true); // true = was a selection
1552                await dotNetRef.invokeMethodAsync(callbackName, item.data);
1553            }
1554        });
1555
1556        return itemDiv;
1557    }
1558
1559    // Leading house number from what the customer typed: "4926 S Quiet Way" -> "4926".
1560    // Accepts a trailing letter and hyphenated numbers ("120B", "1234-A"), both of which
1561    // appear in real US addresses. Requires something after it, so a bare number - which
1562    // is a search in progress, not an address - yields nothing.
1563    function extractLeadingHouseNumber(text) {
1564        const match = /^\s*(\d{1,6}(?:\s*-\s*\d{1,6})?[A-Za-z]?)\s+\S/.exec(text || "");
1565        return match ? match[1].replace(/\s+/g, "") : null;
1566    }
1567
1568    // A street-level result: Google placed the road but not a building on it. Tested by the
1569    // absence of a street_number component rather than by types alone, since a result can
1570    // carry "route" alongside other types.
1571    function isRouteLevelResult(details) {
1572        const components = details?.address_components || [];
1573        const hasStreetNumber = components.some(c => (c.types || []).includes("street_number"));
1574        const types = details?.types || [];
1575        return !hasStreetNumber && types.includes("route");
1576    }
1577
1578    // Fetch place details from Google
1579    async function fetchPlaceDetails(placeId) {
1580        if (!window.google || !google.maps || !google.maps.places) {
1581            console.error("[GMAPS] Google Maps API not loaded");
1582            return;
1583        }
1584
1585        // Captured before closeDropdown() runs, which can rewrite the field. This is the
1586        // only record of the house number when the customer picked a street-level result.
1587        const typedText = inputEl?.value || "";
1588
1589        try {
1590            const service = new google.maps.places.PlacesService(document.createElement('div'));
1591
1592            const details = await new Promise((resolve, reject) => {
1593                service.getDetails({
1594                    placeId: placeId,
1595                    fields: ['address_components', 'formatted_address', 'geometry', 'name', 'place_id', 'types']
1596                }, (result, status) => {
1597                    if (status === google.maps.places.PlacesServiceStatus.OK) {
1598                        resolve(result);
1599                    } else {
1600                        reject(new Error(`Place details failed: ${status}`));
1601                    }
1602                });
1603            });
1604
1605            console.log("[GMAPS] Place details:", details);
1606
1607            // Close dropdown first for immediate feedback, then process callback
1608            closeDropdown(true); // true = was a selection
1609
1610            // Extract street address from components (handles international addresses
1611            // where Google returns street-level info in untyped components)
1612            const parsed = typeof extractAddressComponents === 'function'
1613                ? extractAddressComponents(details.address_components)
1614                : { streetAddress: null };
1615
1616            let finalStreetAddress = parsed.streetAddress;
1617            let finalAddressComponents = details.address_components;
1618            let finalFormattedAddress = details.formatted_address;
1619
1620            // When Google can't place a house number it still offers a prediction showing the
1621            // number the customer typed ("500 S Quiet Way") whose place_id is synthetic - it
1622            // decodes to that literal text - and whose details are only the street. Selecting
1623            // one dropped the number silently, and the step then demanded a street number the
1624            // customer had already supplied. Report it so C# can say which number failed.
1625            // Only ever a diagnosis: nothing is re-resolved, because when Google CAN place the
1626            // number it offers a precise prediction and the customer never lands here.
1627            let unresolvedHouseNumber = null;
1628
1629            if (isRouteLevelResult(details)) {
1630                const typedNumber = extractLeadingHouseNumber(typedText);
1631                if (typedNumber && parsed.streetAddress) {
1632                    console.warn(`[GMAPS] Street-level result - "${typedNumber}" is not a known number on ${parsed.streetAddress}`);
1633                    unresolvedHouseNumber = typedNumber;
1634                }
1635            }
1636
1637            // If no street address found (common for hotels/lodging/POIs), try reverse geocoding
1638            // But ONLY for specific locations (buildings, establishments, POIs) — not for
1639            // cities, states, neighborhoods, or other generic/area-level results
1640            const placeTypes = details.types || [];
1641            const genericAreaTypes = new Set([
1642                'locality', 'political', 'administrative_area_level_1', 'administrative_area_level_2',
1643                'administrative_area_level_3', 'country', 'postal_code', 'neighborhood',
1644                'sublocality', 'sublocality_level_1', 'colloquial_area', 'natural_feature'
1645            ]);
1646            const isGenericArea = placeTypes.length > 0 && placeTypes.every(t => genericAreaTypes.has(t));
1647
1648            if (!finalStreetAddress && details.geometry?.location && !isGenericArea) {
1649                console.log("[GMAPS] No street address from place details, attempting reverse geocode (types:", placeTypes.join(', '), ")");
1650                try {
1651                    const geocoder = new google.maps.Geocoder();
1652                    const latLng = {
1653                        lat: details.geometry.location.lat(),
1654                        lng: details.geometry.location.lng()
1655                    };
1656                    const geocodeResult = await new Promise((resolve, reject) => {
1657                        geocoder.geocode({ location: latLng }, (results, status) => {
1658                            if (status === "OK" && results && results.length > 0) {
1659                                resolve(results);
1660                            } else {
1661                                reject(new Error(`Reverse geocode failed: ${status}`));
1662                            }
1663                        });
1664                    });
1665
1666                    // Find the first result that has a street address
1667                    for (const result of geocodeResult) {
1668                        const reverseComponents = typeof extractAddressComponents === 'function'
1669                            ? extractAddressComponents(result.address_components)
1670                            : { streetAddress: null };
1671                        if (reverseComponents.streetAddress) {
1672                            finalStreetAddress = reverseComponents.streetAddress;
1673                            // Merge street-level components into the original address_components
1674                            // so C# ParseAddressComponents can pick them up
1675                            const existingTypes = new Set();
1676                            finalAddressComponents.forEach(c => (c.types || []).forEach(t => existingTypes.add(t)));
1677                            for (const comp of result.address_components) {
1678                                const compTypes = comp.types || [];
1679                                if (compTypes.includes('street_number') || compTypes.includes('route') || compTypes.includes('postal_code')) {
1680                                    if (!existingTypes.has(compTypes[0])) {
1681                                        finalAddressComponents.push(comp);
1682                                    }
1683                                }
1684                            }
1685                            // Only replace formatted_address if the original didn't contain
1686                            // a meaningful place name (avoid losing hotel/POI names)
1687                            const originalHasName = details.name &&
1688                                finalFormattedAddress.includes(details.name);
1689                            if (!originalHasName && result.formatted_address) {
1690                                finalFormattedAddress = result.formatted_address;
1691                            }
1692                            console.log("[GMAPS] Reverse geocode found street address:", finalStreetAddress);
1693                            break;
1694                        }
1695                    }
1696
1697                    if (!finalStreetAddress) {
1698                        console.log("[GMAPS] Reverse geocode did not yield a street address");
1699                    }
1700                } catch (geoError) {
1701                    console.warn("[GMAPS] Reverse geocode fallback failed:", geoError.message);
1702                }
1703            } else if (!finalStreetAddress && isGenericArea) {
1704                console.log("[GMAPS] Skipping reverse geocode for generic area (types:", placeTypes.join(', '), ")");
1705            }
1706
1707            // Strip place.name when it's just the street address echoed back for plain residential
1708            // premises (Google does this for non-POI addresses). C# also guards against this, but
1709            // clearing here keeps logs and the JS-side place data consistent.
1710            let placeName = details.name || '';
1711            const detailsTypes = details.types || [];
1712            const realPoiTypes = ['lodging', 'tourist_attraction', 'stadium', 'amusement_park',
1713                'aquarium', 'art_gallery', 'casino', 'church', 'city_hall', 'convention_center',
1714                'courthouse', 'embassy', 'hospital', 'library', 'mosque', 'museum',
1715                'rv_park', 'synagogue', 'temple', 'zoo', 'airport'];
1716            const hasRealPoiType = detailsTypes.some(t => realPoiTypes.includes(t));
1717
1718            if (placeName && !hasRealPoiType) {
1719                const nameLower = placeName.trim().toLowerCase();
1720                const streetLower = (finalStreetAddress || '').trim().toLowerCase();
1721                const formattedLower = (finalFormattedAddress || '').trim().toLowerCase();
1722                const nameIsAddress =
1723                    (streetLower && (nameLower === streetLower || nameLower.startsWith(streetLower + ','))) ||
1724                    (formattedLower && (nameLower === formattedLower || formattedLower.startsWith(nameLower + ',')));
1725                if (nameIsAddress) {
1726                    console.log("[GMAPS] Stripping name (matches street address):", placeName);
1727                    placeName = '';
1728                }
1729            }
1730
1731            // Call back to C# with place data
1732            await dotNetRef.invokeMethodAsync(callbackName, {
1733                type: "google_place",
1734                placeId: details.place_id,
1735                formattedAddress: finalFormattedAddress,
1736                name: placeName,
1737                latitude: details.geometry.location.lat(),
1738                longitude: details.geometry.location.lng(),
1739                addressComponents: finalAddressComponents,
1740                types: details.types,
1741                streetAddress: finalStreetAddress,
1742                // Present only when the typed house number could not be placed on this street.
1743                unresolvedHouseNumber: unresolvedHouseNumber,
1744                typedText: typedText
1745            });
1746        } catch (error) {
1747            console.error("[GMAPS] Error fetching place details:", error);
1748        }
1749    }
1750
1751    // Show all airports (serviced only)
1752    function showAllAirports() {
1753        if (!allAirportsData || allAirportsData.length === 0) {
1754            console.warn("[GMAPS] No airports data available");
1755            return;
1756        }
1757
1758        // Filter to serviced airports only
1759        const servicedAirports = allAirportsData.filter(airport => isAirportServiced(airport));
1760        console.log("[GMAPS] Showing all serviced airports:", servicedAirports.length, "of", allAirportsData.length);
1761
1762        const airportsSection = {
1763            title: "All Airports",
1764            icon: "flight",
1765            type: "airports",
1766            items: servicedAirports.map(airport => ({
1767                type: "airport",
1768                icon: getPortIcon(airport.portType || airport.PortType, isDropoffMode ? 1 : 0),
1769                primaryText: `${airport.airportID || airport.AirportID} - ${airport.name || airport.Name}`,
1770                secondaryText: `${airport.city || airport.City}, ${airport.state || airport.State}`,
1771                data: airport
1772            }))
1773        };
1774
1775        renderDropdown({ sections: [airportsSection] });
1776    }
1777
1778    // Filter dropdown based on input
1779    function filterAndFetchResults(query) {
1780        // Reset keyboard navigation when results change
1781        window.__resetKeyboardNavigation(menu);
1782
1783        query = query.trim();
1784
1785        if (query.length === 0) {
1786            currentAirportSection = null;
1787            renderDropdown(originalDropdownData);
1788            return;
1789        }
1790
1791        console.log(`[GMAPS] Filtering for query: "${query}"`);
1792
1793        // Match our own airport records by code or name. Held in currentAirportSection so the
1794        // Places callback can render it above its results instead of wiping it.
1795        currentAirportSection = buildAirportSection(query);
1796
1797        // An exact port code ("LHR", "P27") is unambiguous - show it alone and skip Places,
1798        // which is how a three-character code search has always behaved.
1799        if (currentAirportSection && hasExactCodeMatch(query)) {
1800            console.log("[GMAPS] Exact port code match - showing airports only");
1801            renderDropdown({ sections: [currentAirportSection] });
1802            return;
1803        }
1804
1805        // Filter local data first
1806        const filteredSections = [];
1807
1808        if (originalDropdownData && originalDropdownData.sections) {
1809            originalDropdownData.sections.forEach(section => {
1810                if (!section.items) return;
1811
1812                const matchingItems = section.items.filter(item => {
1813                    const primary = (item.primaryText || "").toLowerCase();
1814                    const secondary = (item.secondaryText || "").toLowerCase();
1815                    const queryLower = query.toLowerCase();
1816
1817                    return primary.includes(queryLower) || secondary.includes(queryLower);
1818                });
1819
1820                if (matchingItems.length > 0) {
1821                    filteredSections.push({
1822                        ...section,
1823                        items: matchingItems
1824                    });
1825                }
1826            });
1827        }
1828
1829        // Our airports go above the nearby/favourite matches - a named airport is a stronger
1830        // hit than a substring match in a saved location.
1831        const localSections = currentAirportSection
1832            ? [currentAirportSection, ...filteredSections]
1833            : filteredSections;
1834
1835        if (localSections.length > 0) {
1836            renderDropdown({ sections: localSections });
1837        } else {
1838            menu.innerHTML = "";
1839        }
1840
1841        // Also query Google Places
1842        if (autocompleteService && query.length >= 3) {
1843            queryGooglePlaces(query);
1844        }
1845    }
1846
1847    // Airports section for the current query, held so Google Places results can be shown
1848    // alongside it rather than replacing it (queryGooglePlaces re-renders the whole menu).
1849    let currentAirportSection = null;
1850
1851    // Maximum airport matches to show, so a broad query ("london") cannot bury the Places
1852    // results underneath a wall of our own records.
1853    const MAX_AIRPORT_MATCHES = 6;
1854
1855    // Search airports/ports by code OR name (searches ALL airports, not just nearby).
1856    // Matches airport codes (PHX), cruise port codes (P27), train station codes, and now the
1857    // airport name and city - typing "heathrow" used to find nothing, because only the code
1858    // was ever compared and only for queries of exactly three characters.
1859    // Note: Non-serviced airports are included for code matches only - clicking them shows an
1860    // error modal (handled in click handler), which is deliberate, so someone asking about a
1861    // specific airport by code learns we know it rather than being told it does not exist.
1862    // They are excluded from name and city matches; see the score check below.
1863    // Returns a dropdown section, or null when nothing matched.
1864    function buildAirportSection(query) {
1865        if (!allAirportsData || allAirportsData.length === 0) {
1866            console.warn("[GMAPS] No airports data for search");
1867            return null;
1868        }
1869
1870        const q = query.trim().toUpperCase();
1871        if (q.length < 2) return null;
1872
1873        // Lower rank = shown first. Code matches beat name matches, and a name that starts
1874        // with the query beats one that merely contains it, so "LHR" and "heathrow" both put
1875        // Heathrow first while "london" still surfaces every London airport.
1876        const scored = [];
1877        allAirportsData.forEach(airport => {
1878            const code = (airport.iataCode || airport.airportID || "").toUpperCase();
1879            const name = (airport.airportName || airport.name || "").toUpperCase();
1880            const city = (airport.city || airport.City || "").toUpperCase();
1881
1882            let score;
1883            if (code === q) score = 0;
1884            else if (code.startsWith(q)) score = 1;
1885            else if (name.startsWith(q)) score = 2;
1886            else if (name.includes(q)) score = 3;
1887            else if (city.startsWith(q)) score = 4;
1888            else return;
1889
1890            // Non-serviced airports are kept for code matches (0-1) but dropped for name and
1891            // city matches (2-4). Typing a code is an explicit question about one airport, and
1892            // "we don't service HHH" answered by the click-through modal beats silence. Typing
1893            // a name is exploratory - someone searching "hilton" wants a hotel, and Hilton Head
1894            // Island is then just a dead end occupying one of MAX_AIRPORT_MATCHES slots.
1895            if (score >= 2 && !isAirportServiced(airport)) return;
1896
1897            scored.push({ airport, score, rank: airport.airportRank ?? airport.AirportRank ?? 999 });
1898        });
1899
1900        if (scored.length === 0) return null;
1901
1902        scored.sort((a, b) => a.score - b.score || a.rank - b.rank);
1903        const matches = scored.slice(0, MAX_AIRPORT_MATCHES).map(s => s.airport);
1904
1905        console.log(`[GMAPS] Airport search "${query}": ${scored.length} match(es), showing ${matches.length}`);
1906
1907        return {
1908            title: "Airports",
1909            icon: "flight",
1910            type: "airports",
1911            items: matches.map(airport => ({
1912                type: "airport",
1913                icon: getPortIcon(airport.portType || airport.PortType, isDropoffMode ? 1 : 0),
1914                primaryText: airport.iataCode || airport.airportID,
1915                secondaryText: airport.airportName || airport.name || "",
1916                data: airport
1917            }))
1918        };
1919    }
1920
1921    // True when the query is an exact hit on a port code, in which case the airport is almost
1922    // certainly what was wanted and there is no reason to wait on Places.
1923    function hasExactCodeMatch(query) {
1924        const q = query.trim().toUpperCase();
1925        return !!allAirportsData && allAirportsData.some(airport =>
1926            (airport.iataCode || airport.airportID || "").toUpperCase() === q);
1927    }
1928
1929    // Query Google Places API
1930    function queryGooglePlaces(query) {
1931        // Retry creating service if it wasn't available at init time
1932        if (!autocompleteService) {
1933            if (window.google?.maps?.places) {
1934                console.log("[GMAPS] Autocomplete service was null, creating now (late init recovery)");
1935                autocompleteService = new google.maps.places.AutocompleteService();
1936            } else {
1937                console.warn("[GMAPS] Autocomplete service not available - Google Maps API not loaded");
1938                return;
1939            }
1940        }
1941
1942        const request = {
1943            input: query,
1944            types: ['geocode', 'establishment']
1945        };
1946
1947        // Add country restriction based on serviced airports
1948        // This limits results to countries where we actually have service
1949        if (servicedCountryCodes.length > 0) {
1950            request.componentRestrictions = { country: servicedCountryCodes };
1951            console.log("[GMAPS] Applying country restriction:", servicedCountryCodes);
1952        }
1953
1954        // Add location bias if available
1955        // NOTE: We intentionally do NOT use locationRestriction (bounds) because it overrides
1956        // the soft bias behavior. Instead, we rely on location+radius for ranking and filter
1957        // results by distance client-side after receiving them.
1958        if (locationBias && locationBias.center) {
1959            // Soft bias - ranks nearby results higher
1960            request.location = new google.maps.LatLng(
1961                locationBias.center.lat,
1962                locationBias.center.lng
1963            );
1964            request.radius = locationBias.radiusMeters || 80000; // ~50 miles default
1965
1966            console.log("[GMAPS] Search bias applied:", {
1967                center: { lat: locationBias.center.lat, lng: locationBias.center.lng },
1968                radiusMiles: Math.round((locationBias.radiusMeters || 80000) / 1609)
1969            });
1970        }
1971
1972        autocompleteService.getPlacePredictions(request, (predictions, status) => {
1973            if (status === google.maps.places.PlacesServiceStatus.OK && predictions) {
1974                console.log('=== GOOGLE AUTOCOMPLETE DATA ===');
1975                predictions.forEach((p, index) => {
1976                    console.log(`Result ${index + 1}:`, {
1977                        description: p.description,
1978                        main_text: p.structured_formatting.main_text,
1979                        secondary_text: p.structured_formatting.secondary_text,
1980                        place_id: p.place_id,
1981                        types: p.types
1982                    });
1983                });
1984                console.log('================================');
1985
1986                const googleSection = {
1987                    title: "Search Results",
1988                    icon: "search",
1989                    items: predictions.map(p => ({
1990                        type: "google_place",
1991                        icon: "place",
1992                        primaryText: p.structured_formatting?.main_text || p.description,
1993                        secondaryText: p.structured_formatting?.secondary_text || "",
1994                        data: { placeId: p.place_id }
1995                    }))
1996                };
1997                // Keep our own airport matches above the Places results - this callback
1998                // re-renders the whole menu, so without this the Airports section built in
1999                // filterAndFetchResults would vanish as soon as Places replied.
2000                renderDropdown({
2001                    sections: currentAirportSection
2002                        ? [currentAirportSection, googleSection]
2003                        : [googleSection]
2004                });
2005            } else {
2006                showNoResults();
2007            }
2008        });
2009    }
2010
2011    // Show no results message
2012    function showNoResults() {
2013        // Places found nothing, but we may still have matched an airport by code or name -
2014        // showing "No results found" over the top of a valid match would be a lie.
2015        if (currentAirportSection) {
2016            renderDropdown({ sections: [currentAirportSection] });
2017            return;
2018        }
2019
2020        menu.innerHTML = "";
2021
2022        // Show "Your Location" at top if we have reverse-geocoded address (and not hiding due to distance)
2023        const currentQuery = inputEl.value.trim();
2024        const isTyping = currentQuery.length > 0;
2025
2026        if (!isTyping && hasNearbyLocation && !hideCurrentLocation) {
2027            // We have a reverse-geocoded address - show "Your Location"
2028            const yourLocationItem = createYourLocationItem(nearbyLocation);
2029            menu.appendChild(yourLocationItem);
2030        }
2031
2032        const noResults = document.createElement("div");
2033        noResults.className = "dropdown-item";
2034        noResults.style.cssText = "color: #5f6368; font-style: italic; justify-content: center; cursor: default; padding: 8px 14px; font-size: 13px;";
2035
2036        // Show encouraging message if query is short (< 5 chars)
2037        if (currentQuery.length > 0 && currentQuery.length < 5) {
2038            noResults.textContent = "Keep typing to search...";
2039        } else {
2040            noResults.textContent = "No results found";
2041        }
2042
2043        noResults.addEventListener("mousedown", e => e.preventDefault());
2044        menu.appendChild(noResults);
2045    }
2046
2047    // Close dropdown
2048    // @param {boolean} wasSelection - true if closing because user made a selection
2049    function closeDropdown(wasSelection = false) {
2050        window.__resetKeyboardNavigation(menu);
2051        menu.innerHTML = "";
2052        inputEl.setAttribute('aria-expanded', 'false');
2053        inputEl.removeAttribute('aria-activedescendant');
2054
2055        // If user made a selection, mark it so we don't show dropdown on re-focus
2056        if (wasSelection) {
2057            hasCompleteSelection = true;
2058            console.log("[GMAPS] Selection made - suppressing future dropdown on focus");
2059        }
2060    }
2061
2062    // Track initial value - if input has a value when initialized, don't auto-show dropdown
2063    const initialValue = inputEl.value.trim();
2064
2065    // Track if user has made a selection (either pre-filled or selected from dropdown)
2066    // When true, suppress dropdown on focus until user clears or modifies the input
2067    let hasCompleteSelection = initialValue.length > 0;
2068
2069    console.log("[GMAPS] Initial value check:", {
2070        initialValue: initialValue || "(empty)",
2071        hasPrefilledValue: initialValue.length > 0,
2072        hasCompleteSelection
2073    });
2074
2075    // If the user typed an exact 3-letter airport code (e.g. "phx"), resolve it
2076    // straight to that airport instead of making them pick from the dropdown.
2077    // Returns true if it matched and a selection was triggered.
2078    const tryResolveAirportCode = (rawValue) => {
2079        const code = (rawValue || "").trim().toUpperCase();
2080        if (code.length !== 3 || !allAirportsData) return false;
2081
2082        const match = allAirportsData.find(a =>
2083            (a.airportID || a.AirportID || a.iataCode || "").toUpperCase() === code);
2084        if (!match) return false;
2085
2086        hasCompleteSelection = true;
2087        closeDropdown();
2088
2089        if (isAirportServiced(match)) {
2090            console.log("[GMAPS] Auto-selected airport by typed code:", code);
2091            dotNetRef.invokeMethodAsync(callbackName, match).catch(() => { });
2092        } else {
2093            console.log("[GMAPS] Typed code is a non-serviced airport:", code);
2094            dotNetRef.invokeMethodAsync("OnNonServicedAirportSelected", match).catch(() => { });
2095        }
2096        return true;
2097    };
2098
2099    // Named event handler functions (needed for clone-and-replace on touch devices)
2100    const handleInput = (e) => {
2101        // User is typing/modifying - reset selection state so dropdown can show
2102        hasCompleteSelection = false;
2103        console.log("[GMAPS] Input changed - reset hasCompleteSelection");
2104        filterAndFetchResults(e.target.value);
2105    };
2106
2107    const handleFocus = () => {
2108        // User is back to fix the address — clear any "choose from the list" flag.
2109        if (dotNetRef) {
2110            dotNetRef.invokeMethodAsync("ClearUnselectedFlag").catch(() => { });
2111        }
2112
2113        const currentValue = inputEl.value.trim();
2114
2115        // If user has made a complete selection (either pre-filled or selected from dropdown),
2116        // suppress dropdown until they clear or modify the input
2117        if (hasCompleteSelection && currentValue.length > 0) {
2118            console.log("[GMAPS] Suppressing dropdown - has complete selection");
2119            return;
2120        }
2121
2122        // On touch devices (iPad/iPhone), delay dropdown rendering until the
2123        // virtual keyboard has fully appeared. DOM manipulation during focus
2124        // causes Safari to suppress the keyboard entirely.
2125        // On desktop, render immediately via requestAnimationFrame.
2126        const isTouchDevice = navigator.maxTouchPoints > 0;
2127
2128        const showDropdown = () => {
2129            // Verify input is still focused (user may have tapped away)
2130            if (document.activeElement !== inputEl) return;
2131
2132            if (currentValue.length === 0) {
2133                // Only render dropdown if we have data
2134                if (originalDropdownData && originalDropdownData.sections) {
2135                    renderDropdown(originalDropdownData);
2136                }
2137            } else {
2138                filterAndFetchResults(currentValue);
2139            }
2140        };
2141
2142        // Clone-and-replace strips Blazor's async event listeners on touch devices,
2143        // so DOM manipulation during focus no longer suppresses the keyboard.
2144        // Use requestAnimationFrame for all devices for responsive dropdown display.
2145        requestAnimationFrame(showDropdown);
2146    };
2147
2148    const handleBlur = () => {
2149        setTimeout(() => {
2150            closeDropdown();
2151            // If the user typed something but never picked a suggestion, no address was
2152            // captured. Flag the field so they get immediate feedback (rather than a box
2153            // that looks filled but silently fails when they click Continue). The 150ms
2154            // delay lets a dropdown-item click register first (which sets hasCompleteSelection).
2155            if (!hasCompleteSelection && inputEl.value.trim().length > 0 && dotNetRef) {
2156                // If they typed an exact airport code, resolve it instead of flagging.
2157                if (tryResolveAirportCode(inputEl.value)) return;
2158                dotNetRef.invokeMethodAsync("FlagUnselectedBlur").catch(() => { });
2159            }
2160        }, 150);
2161    };
2162
2163    const handleKeydown = (e) => {
2164        // Exact airport code + Enter → resolve immediately, unless the user is actively
2165        // highlighting a dropdown item (that case is handled by the navigation logic below).
2166        if (e.key === "Enter" &&
2167            (window.__keyboardHighlightIndex == null || window.__keyboardHighlightIndex < 0)) {
2168            if (tryResolveAirportCode(inputEl.value)) {
2169                e.preventDefault();
2170                return;
2171            }
2172        }
2173
2174        // Handle keyboard navigation in dropdown
2175        // Check visibility more robustly - menu has content and is attached to DOM
2176        const hasContent = menu && menu.innerHTML.trim() !== '';
2177        const isAttached = menu && menu.parentElement !== null;
2178        const hasChildren = menu && menu.children.length > 0;
2179        const isDropdownVisible = hasContent && isAttached && hasChildren;
2180
2181        // Debug logging
2182        if (e.key === "ArrowDown" || e.key === "ArrowUp") {
2183            console.log("[GMAPS] Keyboard nav debug:", {
2184                key: e.key,
2185                hasContent,
2186                isAttached,
2187                hasChildren,
2188                isDropdownVisible,
2189                menuChildCount: menu?.children?.length,
2190                highlightIndex: window.__keyboardHighlightIndex
2191            });
2192        }
2193
2194        if (isDropdownVisible) {
2195            const items = window.__getSelectableItems(menu);
2196
2197            console.log("[GMAPS] Selectable items:", items.length);
2198
2199            if (items.length > 0) {
2200                switch (e.key) {
2201                    case "ArrowDown":
2202                        e.preventDefault();
2203                        window.__keyboardHighlightIndex = Math.min(
2204                            window.__keyboardHighlightIndex + 1,
2205                            items.length - 1
2206                        );
2207                        console.log("[GMAPS] ArrowDown - new index:", window.__keyboardHighlightIndex);
2208                        window.__highlightItemAtIndex(menu, window.__keyboardHighlightIndex, inputEl);
2209                        return;
2210
2211                    case "ArrowUp":
2212                        e.preventDefault();
2213                        if (window.__keyboardHighlightIndex <= 0) {
2214                            window.__keyboardHighlightIndex = 0;
2215                        } else {
2216                            window.__keyboardHighlightIndex--;
2217                        }
2218                        console.log("[GMAPS] ArrowUp - new index:", window.__keyboardHighlightIndex);
2219                        window.__highlightItemAtIndex(menu, window.__keyboardHighlightIndex, inputEl);
2220                        return;
2221
2222                    case "Enter":
2223                    case "Tab":
2224                        if (window.__keyboardHighlightIndex >= 0 && window.__keyboardHighlightIndex < items.length) {
2225                            e.preventDefault();
2226                            console.log("[GMAPS] " + e.key + " - selecting index:", window.__keyboardHighlightIndex);
2227                            const selectedItem = items[window.__keyboardHighlightIndex];
2228                            selectedItem.click();
2229                            window.__keyboardHighlightIndex = -1;
2230                            // Tab should also advance focus to the next tabbable field
2231                            if (e.key === "Tab") {
2232                                const tabbableSelector = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
2233                                const all = Array.from(document.querySelectorAll(tabbableSelector))
2234                                    .filter(el => el.offsetParent !== null && el.getAttribute('tabindex') !== '-1');
2235                                const idx = all.indexOf(inputEl);
2236                                const next = e.shiftKey
2237                                    ? (idx > 0 ? all[idx - 1] : null)
2238                                    : (idx >= 0 && idx + 1 < all.length ? all[idx + 1] : null);
2239                                if (next) {
2240                                    console.log("[GMAPS] Tab - advancing focus to:", next.id || next.tagName);
2241                                    next.focus();
2242                                }
2243                            }
2244                            return;
2245                        }
2246                        break;
2247                }
2248            }
2249        }
2250
2251        // Escape key handling
2252        if (e.key === "Escape") {
2253            window.__resetKeyboardNavigation(menu);
2254            closeDropdown();
2255            inputEl.blur();
2256        }
2257    };
2258
2259    const handleTouchClick = () => {
2260        if (!inputEl.readOnly) {
2261            inputEl.focus();
2262        }
2263    };
2264
2265    // Attach event listeners
2266    inputEl.addEventListener("input", handleInput);
2267    inputEl.addEventListener("focus", handleFocus);
2268    inputEl.addEventListener("blur", handleBlur);
2269    inputEl.addEventListener("keydown", handleKeydown);
2270
2271    // iPad Safari keyboard fix: On iPad (wide-screen touch devices), Blazor's async
2272    // WASM event dispatch (from @onfocus/@onclick handlers) intercepts the native
2273    // focus/click events and breaks iPad Safari's user-gesture chain, preventing the
2274    // virtual keyboard from appearing on first tap. Solution: clone the input element
2275    // to strip ALL event listeners (including Blazor's), then re-attach only our
2276    // native JS listeners. cloneNode(true) copies attributes but NOT event listeners.
2277    // NOTE: Only do this for iPad/tablet (wide screen + touch). iPhone uses a modal
2278    // for address entry and doesn't have this keyboard issue — cloning there breaks
2279    // Blazor's @oninput sync and the modal's select/clear workflow.
2280    const isTouchDevice = navigator.maxTouchPoints > 0;
2281    const isTabletTouch = isTouchDevice && window.innerWidth >= 768;
2282    if (isTabletTouch) {
2283        console.log("[GMAPS] Touch device detected - cloning input to strip Blazor event listeners");
2284        // Save JS properties that cloneNode doesn't copy (it only copies DOM attributes)
2285        const savedAcState = inputEl.__acState;
2286        const savedNearbyHandler = inputEl.__nearbyLocationHandler;
2287
2288        const clone = inputEl.cloneNode(true);
2289        inputEl.parentNode.replaceChild(clone, inputEl);
2290        // Update inputEl to point to the clone — all closure references now use the new element
2291        inputEl = clone;
2292
2293        // Restore JS properties on the clone so updateAutocompleteDropdown can find them
2294        clone.__acState = savedAcState;
2295        if (savedNearbyHandler) {
2296            clone.__nearbyLocationHandler = savedNearbyHandler;
2297        }
2298
2299        // Re-attach our native event listeners on the clean clone
2300        clone.addEventListener("input", handleInput);
2301        clone.addEventListener("focus", handleFocus);
2302        clone.addEventListener("blur", handleBlur);
2303        clone.addEventListener("keydown", handleKeydown);
2304        clone.addEventListener("click", handleTouchClick);
2305    }
2306
2307    // Initial render after short delay (only if we have dropdown data AND no pre-filled value)
2308    // Skip for inline mode (skipInitialDropdown flag) - dropdown shows on focus instead
2309    // Skip for touch devices - DOM manipulation during init suppresses keyboard on iPad Safari
2310    const isTouchInit = isTabletTouch;
2311    setTimeout(() => {
2312        if (locationBias?.skipInitialDropdown) {
2313            console.log("[GMAPS] Skipping initial dropdown render - inline mode");
2314        } else if (isTouchInit) {
2315            console.log("[GMAPS] Skipping initial dropdown render - touch device (will render on focus)");
2316        } else if (originalDropdownData && originalDropdownData.sections && initialValue.length === 0) {
2317            renderDropdown(originalDropdownData);
2318        } else if (initialValue.length > 0) {
2319            console.log("[GMAPS] Skipping initial dropdown render - input has pre-filled value");
2320        }
2321    }, 50);
2322
2323    console.log("[GMAPS] Autocomplete initialized");
2324};
2325
2326// Clear autocomplete input value and hide dropdown
2327window.clearAutocompleteInput = function (inputId) {
2328    const inputEl = document.getElementById(inputId);
2329    if (inputEl) {
2330        inputEl.value = '';
2331        // Hide any open dropdown
2332        if (window.__acMenus) {
2333            window.__acMenus.forEach(menu => {
2334                if (menu?.style) {
2335                    menu.style.display = 'none';
2336                }
2337            });
2338        }
2339    }
2340};
2341
2342// Set autocomplete input value (used when converting addresses to airports)
2343window.setAutocompleteInputValue = function (inputId, value) {
2344    const inputEl = document.getElementById(inputId);
2345    if (inputEl) {
2346        inputEl.value = value;
2347    }
2348};
2349
2350// Trigger autocomplete search for a pre-filled address (chatbot deep links)
2351// This simulates user input to show the dropdown with matching results
2352window.triggerAutocompleteSearch = function (inputId, searchText) {
2353    const inputEl = document.getElementById(inputId);
2354    if (!inputEl) {
2355        console.error('[GMAPS] triggerAutocompleteSearch: input element not found:', inputId);
2356        return;
2357    }
2358
2359    console.log('[GMAPS] triggerAutocompleteSearch:', inputId, searchText);
2360
2361    // Focus the input first
2362    inputEl.focus();
2363
2364    // Dispatch an input event to trigger the autocomplete search
2365    // This mimics the user typing and will show the dropdown
2366    const inputEvent = new Event('input', { bubbles: true, cancelable: true });
2367    inputEl.dispatchEvent(inputEvent);
2368};
2369
2370// Update autocomplete dropdown data (called when pickup changes and we need to update dropoff options)
2371window.updateAutocompleteDropdown = function (inputId, newDropdownData, newAirportsData, newLocationBias) {
2372    const inputEl = document.getElementById(inputId);
2373    if (!inputEl) {
2374        console.error('[GMAPS] updateAutocompleteDropdown: input element not found:', inputId);
2375        return;
2376    }
2377
2378    // Check if this input has autocomplete state stored
2379    if (!inputEl.__acState) {
2380        console.warn('[GMAPS] updateAutocompleteDropdown: no autocomplete state found for:', inputId);
2381        return;
2382    }
2383
2384    console.log("[GMAPS] updateAutocompleteDropdown for:", inputId, {
2385        sections: newDropdownData?.sections?.length,
2386        airports: newAirportsData?.length,
2387        suppressAirports: newLocationBias?.suppressAirports
2388    });
2389
2390    // Update the stored state
2391    inputEl.__acState.dropdownData = newDropdownData;
2392    inputEl.__acState.allAirportsData = newAirportsData;
2393    inputEl.__acState.locationBias = newLocationBias;
2394
2395    // Update the closure variables via the stored update function
2396    if (inputEl.__acState.updateData) {
2397        inputEl.__acState.updateData(newDropdownData, newAirportsData, newLocationBias);
2398    }
2399};
2400
2401// Cleanup function
2402window.cleanupAutocomplete = function (inputEl) {
2403    // Remove nearbyLocation event listener if attached
2404    if (inputEl?.__nearbyLocationHandler) {
2405        window.removeEventListener('nearbyLocation:ready', inputEl.__nearbyLocationHandler);
2406        delete inputEl.__nearbyLocationHandler;
2407    }
2408
2409    if (window.__acMenus) {
2410        window.__acMenus.forEach(menu => {
2411            if (menu?.parentElement) {
2412                menu.remove();
2413            }
2414        });
2415        window.__acMenus.clear();
2416    }
2417};
2418
2419console.log("[GOOGLEMAPS-DROPDOWN] Finished loading", window.__gmVersion, "- check for 'Your Location' display");

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.