PageSourceSearch

https://services.unitedbaristas.com/wp-content/plugins/jreviews_ad…?v=1787990148&ver=5.12.0

js unitedbaristas.com collected 2026-10-04 02:40:17 UTC 44,910 bytes, 964 lines download raw bytes

1// Languages the Mapbox Search Box / Geocoding APIs localize results into.
2// Anything outside this set is left unset so Mapbox returns its default rather
3// than erroring. https://docs.mapbox.com/api/search/search-box/
4// Shared global: both alpine-location-autocomplete.js and alpine-proximity-search.js
5// can load on the same page. Guard the assignment so the second script does not
6// redeclare a lexical `const` (which throws and aborts the whole file).
7window.JR_MAPBOX_LANGUAGES = window.JR_MAPBOX_LANGUAGES || ['cs', 'hr', 'da', 'nl', 'en', 'et', 'fi', 'fr', 'de', 'el', 'hu', 'it', 'ja', 'lt', 'lv', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'es', 'sv', 'tr', 'uk'];
8
9/**
10 * Register on whichever comes first. See the matching note in
11 * alpine-location-autocomplete.js: relying on alpine:init alone means this file
12 * has to be on the page before Alpine starts, and arriving any later would leave
13 * every widget that names it inert with nothing to show for it.
14 */
15function jrRegisterProximitySearch() {
16    if (!window.Alpine || window.jrProximitySearchRegistered) return;
17    window.jrProximitySearchRegistered = true;
18
19    Alpine.data('jrMapsProximitySearch', (config = {}) => {
20        const globalConfig = window.jrMapsAutocomplete || {};
21
22        return {
23            query: config.address || '',
24            lat: config.lat ?? '',
25            lng: config.lng ?? '',
26            radius: String(config.radius ?? config.defaultRadius ?? 30),
27            defaultRadius: Number(config.defaultRadius ?? 30),
28            autoResolveOnBlur: config.autoResolveOnBlur ?? true,
29            maxRadius: Number(config.maxRadius ?? 100),
30            metric: config.metric || 'km',
31            autocompleteEnabled: config.autocompleteEnabled ?? globalConfig.autocompleteEnabled ?? false,
32            suggestions: [],
33            selectedIndex: -1,
34            isOpen: false,
35            isLoading: false,
36            debounceTimer: null,
37            apiLoaded: false,
38            apiLoading: false,
39            apiType: null,
40            autocompleteService: null,
41            placesService: null,
42            locationSearchClient: null,
43            geocodingService: globalConfig.geocodingService,
44            countryRestriction: config.countryRestriction ?? globalConfig.countryRestriction ?? '',
45            minChars: globalConfig.minChars || 3,
46            debounceDelay: globalConfig.debounceDelay || 600,
47            googleApiKey: globalConfig.googleApiKey || null,
48            googleApiLocale: globalConfig.googleApiLocale || 'en',
49            googleApiUrl: globalConfig.googleApiUrl || null,
50            geocodeLang: globalConfig.geocodeLang || '',
51            mapboxToken: globalConfig.mapboxToken || null,
52            coordinatorScript: globalConfig.coordinatorScript,
53            lang: globalConfig.lang || {
54                placeholder: 'Enter a location',
55                searchPrompt: 'Start typing to search...',
56                noResults: 'No locations found',
57                unavailable: 'Location search is unavailable right now. Please try again shortly.'
58            },
59            geolocating: false,
60            // The provider did not answer, as distinct from answering with nothing.
61            isUnavailable: false,
62
63            get isActive() {
64                return this.query.trim() !== '' || this.lat !== '' || this.lng !== '';
65            },
66
67            radiusLabel() {
68                return `${this.radius} ${this.metric}`;
69            },
70
71            emptyMessage() {
72                if (this.isLoading) return '';
73                if (this.isUnavailable) return this.lang.unavailable;
74                return !this.query ? this.lang.searchPrompt : this.lang.noResults;
75            },
76            async handleFocus() {
77                if (!this.autocompleteEnabled || this.query.length < this.minChars) {
78                    return;
79                }
80
81                if (this.suggestions.length > 0) {
82                    this.isOpen = true;
83                    return;
84                }
85
86                await this.fetchSuggestions();
87            },
88
89            async handleBlur(event) {
90                const nextTarget = event.relatedTarget;
91
92                if (nextTarget && nextTarget.closest('[data-jr-proximity-search]')) {
93                    return;
94                }
95
96                if (!this.autoResolveOnBlur || !this.query.trim() || (this.lat !== '' && this.lng !== '')) {
97                    return;
98                }
99
100                await this.ensureResolvedForSubmit();
101            },
102
103            handleInput(event) {
104                this.query = event.target.value;
105                clearTimeout(this.debounceTimer);
106                this.clearCoordinates(false);
107
108                if (this.query.length === 0) {
109                    this.closeSuggestions();
110                    this.dispatchChange();
111                    return;
112                }
113
114                if (!this.autocompleteEnabled || this.query.length < this.minChars) {
115                    this.closeSuggestions();
116                    return;
117                }
118
119                this.debounceTimer = setTimeout(() => this.fetchSuggestions(), this.debounceDelay);
120            },
121
122            async handleKeydown(event) {
123                if (this.isOpen && this.suggestions.length > 0) {
124                    switch (event.key) {
125                        case 'ArrowDown':
126                            event.preventDefault();
127                            this.selectedIndex = Math.min(this.selectedIndex + 1, this.suggestions.length - 1);
128                            return;
129                        case 'ArrowUp':
130                            event.preventDefault();
131                            this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
132                            return;
133                        case 'Enter':
134                            event.preventDefault();
135                            if (this.selectedIndex >= 0 && this.selectedIndex < this.suggestions.length) {
136                                await this.selectSuggestion(this.suggestions[this.selectedIndex]);
137                            } else {
138                                await this.resolveAddress();
139                            }
140                            return;
141                        case 'Escape':
142                            this.closeSuggestions();
143                            return;
144                    }
145                }
146
147                if (event.key === 'Enter' && this.query.trim() !== '' && (this.lat === '' || this.lng === '')) {
148                    event.preventDefault();
149                    await this.resolveAddress();
150                }
151            },
152
153            async resolveAddress() {
154                if (this.query.trim().length < this.minChars) {
155                    return false;
156                }
157
158                const suggestions = await this.fetchSuggestions(true);
159
160                if (suggestions.length > 0) {
161                    await this.selectSuggestion(suggestions[0]);
162                    return this.lat !== '' && this.lng !== '';
163                }
164
165                return false;
166            },
167
168            async ensureResolvedForSubmit() {
169                if (this.query.trim() === '' || (this.lat !== '' && this.lng !== '')) {
170                    return this.lat !== '' && this.lng !== '';
171                }
172
173                return await this.resolveAddress();
174            },
175
176            async loadAPIs() {
177                if (this.apiLoaded) return;
178                if (this._apiLoadPromise) return this._apiLoadPromise;
179                this._apiLoadPromise = this._doLoadAPIs();
180                return this._apiLoadPromise;
181            },
182
183            async _doLoadAPIs() {
184                this.apiLoading = true;
185
186                if (this.geocodingService === 'google' && this.googleApiKey) {
187                    if (typeof window.jrMapsApi === 'undefined' && this.coordinatorScript) {
188                        await this.loadScript(this.coordinatorScript);
189                    }
190
191                    if (typeof google !== 'undefined' && typeof google.maps !== 'undefined' && typeof google.maps.places !== 'undefined') {
192                        this.apiType = this.detectGooglePlacesAPI();
193                    } else if (window.jrMapsApi) {
194                        await window.jrMapsApi.loadGoogle(this.googleApiKey, {
195                            libraries: ['places'],
196                            locale: this.googleApiLocale,
197                            apiUrl: this.googleApiUrl
198                        });
199                        this.apiType = this.detectGooglePlacesAPI();
200                    }
201
202                    if (this.apiType === 'legacy') {
203                        this.ensureLegacyGoogleServices();
204                    }
205                }
206
207                this.apiLoaded = true;
208                this.apiLoading = false;
209            },
210
211            /**
212             * Load a file on demand. See the matching note in
213             * alpine-location-autocomplete.js: a tag being present is not the
214             * same as the file having run, and two location widgets on one page
215             * is the ordinary case that exposed it.
216             */
217            loadScript(url) {
218                if (window.jrMapsApi?.ensureScript) {
219                    return window.jrMapsApi.ensureScript(url);
220                }
221
222                if (!url) return Promise.reject(new Error('A script URL is required.'));
223
224                window.jrMapsCoordinatorPromise = window.jrMapsCoordinatorPromise
225                    || new Promise((resolve, reject) => {
226                        if (window.jrMapsApi) return resolve();
227
228                        const existing = document.querySelector(`script[src="${url}"]`);
229                        const script = existing || document.createElement('script');
230
231                        script.addEventListener('load', () => resolve(), { once: true });
232                        script.addEventListener('error', reject, { once: true });
233
234                        if (existing) {
235                            if (window.jrMapsApi) return resolve();
236                            window.addEventListener('load', () => resolve(), { once: true });
237                            return;
238                        }
239
240                        script.src = url;
241                        script.async = false;
242                        document.head.appendChild(script);
243                    });
244
245                return window.jrMapsCoordinatorPromise;
246            },
247
248            detectGooglePlacesAPI() {
249                if (typeof google === 'undefined' || typeof google.maps === 'undefined' || typeof google.maps.places === 'undefined') {
250                    return null;
251                }
252
253                if (
254                    typeof google.maps.places.AutocompleteSuggestion !== 'undefined'
255                    && typeof google.maps.places.Place !== 'undefined'
256                    && typeof google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions === 'function'
257                ) {
258                    try {
259                        const place = new google.maps.places.Place({ id: 'test' });
260                        if (typeof place.fetchFields === 'function') {
261                            return 'new';
262                        }
263                    } catch (e) {}
264                }
265
266                return typeof google.maps.places.AutocompleteService !== 'undefined' ? 'legacy' : null;
267            },
268
269            effectiveGoogleApiType() {
270                return this.apiType;
271            },
272
273            ensureLegacyGoogleServices() {
274                if (
275                    typeof google === 'undefined'
276                    || typeof google.maps === 'undefined'
277                    || typeof google.maps.places === 'undefined'
278                    || typeof google.maps.places.AutocompleteService === 'undefined'
279                ) {
280                    return false;
281                }
282
283                if (!this.autocompleteService) {
284                    this.autocompleteService = new google.maps.places.AutocompleteService();
285                }
286
287                if (!this.placesService) {
288                    const div = document.createElement('div');
289                    this.placesService = new google.maps.places.PlacesService(div);
290                }
291
292                return true;
293            },
294
295            async fetchSuggestions(returnResults = false) {
296                this.isLoading = true;
297                this.isOpen = true;
298
299                try {
300                    this.locationSearchClient ||= window.jrMapsLocationSearch?.create({
301                        ...globalConfig,
302                        countryRestriction: this.countryRestriction
303                    });
304                    const provider = this.suggestionProviders[this.geocodingService];
305                    this.suggestions = this.locationSearchClient
306                        ? await this.locationSearchClient.suggest(this.query, { explicit: returnResults })
307                        : provider ? await provider.call(this, this.query) : [];
308                    // The provider answered; an empty list is the honest empty state.
309                    this.isUnavailable = false;
310                    this.selectedIndex = -1;
311                } catch (error) {
312                    console.error('Failed to fetch proximity suggestions:', error);
313                    // A throw means it did not answer. Closing silently left the
314                    // visitor with no explanation at all.
315                    this.suggestions = [];
316                    this.selectedIndex = -1;
317                    this.isUnavailable = true;
318                    this.isOpen = true;
319                    window.jrMapsLocationSearch?.report?.(error, this.geocodingService);
320                } finally {
321                    this.isLoading = false;
322                }
323
324                return returnResults ? this.suggestions : undefined;
325            },
326
327            async selectSuggestion(suggestion) {
328                const placeDetails = await this.fetchPlaceDetails(suggestion);
329                this.query = placeDetails?.formatted_address || suggestion.label;
330                this.lat = placeDetails?.lat ?? '';
331                this.lng = placeDetails?.lng ?? '';
332                this.closeSuggestions();
333                this.dispatchChange();
334            },
335
336            async fetchPlaceDetails(suggestion) {
337                try {
338                    if (this.locationSearchClient) {
339                        return await this.locationSearchClient.resolve(suggestion);
340                    }
341                    const provider = this.placeDetailProviders[this.geocodingService];
342                    return provider ? await provider.call(this, suggestion) : null;
343                } catch (error) {
344                    console.error('Failed to fetch proximity place details:', error);
345                    return null;
346                }
347            },
348
349            closeSuggestions() {
350                this.isOpen = false;
351                this.selectedIndex = -1;
352            },
353
354            clearCoordinates(dispatch = true) {
355                this.lat = '';
356                this.lng = '';
357                if (dispatch) this.dispatchChange();
358            },
359
360            clear() {
361                this.query = '';
362                this.radius = String(this.defaultRadius);
363                this.clearCoordinates(false);
364                const root = this.$root || this.$el || null;
365                if (root) {
366                    ['geosearch[address]', 'geosearch[lat]', 'geosearch[lng]', 'geosearch[radius]'].forEach(name => {
367                        root.querySelectorAll(`input[name="${name}"]`).forEach(input => {
368                            input.value = '';
369                        });
370                    });
371                }
372                if (this.$refs?.address) {
373                    this.$refs.address.value = '';
374                }
375                this.closeSuggestions();
376                this.dispatchChange();
377            },
378
379            async useMyLocation() {
380                if (this.geolocating) return;
381
382                if (!navigator.geolocation) {
383                    console.warn('Geolocation is not supported in this browser.');
384                    return;
385                }
386
387                this.geolocating = true;
388                this.isLoading = true;
389                this.closeSuggestions();
390
391                try {
392                    const position = await new Promise((resolve, reject) => {
393                        navigator.geolocation.getCurrentPosition(resolve, reject, {
394                            enableHighAccuracy: false,
395                            timeout: 10000,
396                            maximumAge: 60000,
397                        });
398                    });
399
400                    const lat = position.coords.latitude;
401                    const lng = position.coords.longitude;
402
403                    if (!this.apiLoaded) {
404                        await this.loadAPIs();
405                    }
406
407                    let address = '';
408                    try {
409                        const provider = this.reverseGeocodeProviders[this.geocodingService];
410                        address = provider ? await provider.call(this, lat, lng) : '';
411                    } catch (error) {
412                        console.error('Reverse geocoding failed:', error);
413                    }
414
415                    this.lat = lat;
416                    this.lng = lng;
417                    this.query = address || `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
418                    this.dispatchChange();
419                } catch (error) {
420                    console.error('Geolocation error:', error);
421                    const messages = {
422                        1: this.lang.geolocationDenied || 'Location permission denied. Please allow location access in your browser.',
423                        2: this.lang.geolocationUnavailable || 'Could not determine your location. Please try again or enter an address manually.',
424                        3: this.lang.geolocationTimeout || 'Location request timed out. Please try again.',
425                    };
426                    const message = messages[error?.code] || (this.lang.geolocationFailed || 'Could not get your location.');
427                    if (typeof window.alert === 'function') {
428                        window.alert(message);
429                    }
430                } finally {
431                    this.geolocating = false;
432                    this.isLoading = false;
433                }
434            },
435
436            reverseGeocodeProviders: {
437                google: async function(lat, lng) {
438                    if (typeof google === 'undefined' || !google.maps?.Geocoder) return '';
439                    const geocoder = new google.maps.Geocoder();
440                    return new Promise(resolve => {
441                        geocoder.geocode({ location: { lat, lng } }, (results, status) => {
442                            resolve(status === 'OK' && results?.[0] ? results[0].formatted_address : '');
443                        });
444                    });
445                },
446                nominatim: async function(lat, lng) {
447                    const url = new URL('https://nominatim.openstreetmap.org/reverse');
448                    url.searchParams.set('lat', lat);
449                    url.searchParams.set('lon', lng);
450                    url.searchParams.set('format', 'json');
451                    if (this.geocodeLang) url.searchParams.set('accept-language', this.geocodeLang);
452                    const response = await fetch(url);
453                    const data = await response.json();
454                    return data?.display_name || '';
455                },
456                mapbox: async function(lat, lng) {
457                    const params = new URLSearchParams({
458                        longitude: lng,
459                        latitude: lat,
460                        access_token: this.mapboxToken,
461                        limit: 1,
462                    });
463                    if (this.geocodeLang && JR_MAPBOX_LANGUAGES.includes(this.geocodeLang)) params.set('language', this.geocodeLang);
464                    const response = await fetch(`https://api.mapbox.com/search/geocode/v6/reverse?${params}`);
465                    const data = await response.json();
466                    const props = data?.features?.[0]?.properties;
467                    return props?.full_address || props?.name || '';
468                },
469                arcgis: async function(lat, lng) {
470                    const params = new URLSearchParams({
471                        location: `${lng},${lat}`,
472                        f: 'json',
473                    });
474                    if (this.geocodeLang) params.set('langCode', this.geocodeLang);
475                    const response = await fetch(`https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode?${params}`);
476                    const data = await response.json();
477                    return data?.address?.LongLabel || data?.address?.Match_addr || '';
478                },
479                photon: async function(lat, lng) {
480                    const url = new URL('https://photon.komoot.io/reverse');
481                    url.searchParams.set('lat', lat);
482                    url.searchParams.set('lon', lng);
483                    url.searchParams.set('limit', 1);
484                    if (this.geocodeLang) url.searchParams.set('lang', this.geocodeLang);
485                    const response = await fetch(url);
486                    const data = await response.json();
487                    const feature = data?.features?.[0];
488                    if (!feature) return '';
489                    const props = feature.properties || {};
490                    return [props.name, props.city, props.state, props.country].filter(Boolean).join(', ');
491                },
492            },
493
494            dispatchChange() {
495                this.$nextTick(() => {
496                    if (this.$refs.address) {
497                        this.$refs.address.dispatchEvent(new Event('change', { bubbles: true }));
498                    }
499                });
500            },
501
502            async _fetchSuggestionsNewAPI(query) {
503                const request = { input: query };
504                if (this.countryRestriction) request.includedRegionCodes = [this.countryRestriction];
505                const { suggestions } = await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(request);
506                return suggestions.map(suggestion => {
507                    const pred = suggestion.placePrediction;
508                    return {
509                        id: pred.placeId,
510                        label: pred.text.text,
511                        ref: pred.placeId,
512                        primary: pred.structuredFormat?.mainText?.text || pred.text.text,
513                        secondary: pred.structuredFormat?.secondaryText?.text || ''
514                    };
515                });
516            },
517
518            async _fetchSuggestionsLegacyAPI(query) {
519                return new Promise(resolve => {
520                    const request = { input: query };
521                    if (this.countryRestriction) request.componentRestrictions = { country: this.countryRestriction };
522                    this.autocompleteService.getPlacePredictions(request, (predictions, status) => {
523                        if (status === google.maps.places.PlacesServiceStatus.OK && predictions) {
524                            resolve(predictions.map(prediction => ({
525                                id: prediction.place_id,
526                                label: prediction.description,
527                                ref: prediction.place_id,
528                                primary: prediction.structured_formatting?.main_text || prediction.description,
529                                secondary: prediction.structured_formatting?.secondary_text || ''
530                            })));
531                            return;
532                        }
533                        resolve([]);
534                    });
535                });
536            },
537
538            suggestionProviders: {
539                google: async function(query) {
540                    const apiType = this.effectiveGoogleApiType();
541                    if (apiType === 'new') return this._fetchSuggestionsNewAPI(query);
542                    if (apiType === 'legacy') return this._fetchSuggestionsLegacyAPI(query);
543                    return [];
544                },
545                nominatim: async function(query) {
546                    const url = new URL('https://nominatim.openstreetmap.org/search');
547                    url.searchParams.set('q', query);
548                    url.searchParams.set('format', 'json');
549                    url.searchParams.set('limit', 10);
550                    url.searchParams.set('addressdetails', 1);
551                    if (this.countryRestriction) url.searchParams.set('countrycodes', this.countryRestriction);
552                    if (this.geocodeLang) url.searchParams.set('accept-language', this.geocodeLang);
553                    const response = await fetch(url);
554                    const data = await response.json();
555                    const skipTypes = ['yes', 'house', 'residential', 'building', 'address', 'apartments', 'detached', 'terrace'];
556                    return data.map(item => {
557                        const parts = item.display_name.split(',').map(p => p.trim());
558                        let primary = parts[0] || item.display_name;
559                        const secondaryParts = [];
560                        const county = item.address?.county;
561                        if (county && !primary.includes(county)) {
562                            primary = `${primary}, ${county}`;
563                        }
564                        if (item.type && !skipTypes.includes(item.type)) {
565                            secondaryParts.push(item.type);
566                        }
567                        if (item.address?.state && !primary.includes(item.address.state)) {
568                            secondaryParts.push(item.address.state);
569                        }
570                        if (item.address?.country && !primary.includes(item.address.country)) {
571                            secondaryParts.push(item.address.country);
572                        }
573                        return {
574                            id: item.place_id,
575                            label: item.display_name,
576                            ref: item,
577                            data: item,
578                            primary,
579                            secondary: secondaryParts.join(', ')
580                        };
581                    });
582                },
583                mapbox: async function(query) {
584                    if (!this.mapboxSessionToken) this.mapboxSessionToken = crypto.randomUUID();
585                    const params = new URLSearchParams({
586                        q: query,
587                        access_token: this.mapboxToken,
588                        session_token: this.mapboxSessionToken,
589                        limit: 10
590                    });
591                    if (this.countryRestriction) params.set('country', this.countryRestriction);
592                    if (this.geocodeLang && JR_MAPBOX_LANGUAGES.includes(this.geocodeLang)) params.set('language', this.geocodeLang);
593                    const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
594                    const data = await response.json();
595                    return (data.suggestions || []).map((suggestion, index) => {
596                        let primary = suggestion.name || suggestion.full_address;
597                        const secondaryParts = [];
598                        const place = suggestion.context?.place?.name;
599                        if (place && !primary.includes(place)) {
600                            primary = `${primary}, ${place}`;
601                        }
602                        if (suggestion.poi_category && suggestion.poi_category.length) {
603                            secondaryParts.push(suggestion.poi_category[0]);
604                        } else if (suggestion.feature_type && suggestion.feature_type !== 'address') {
605                            secondaryParts.push(suggestion.feature_type);
606                        }
607                        if (suggestion.context?.region?.name && !primary.includes(suggestion.context.region.name)) {
608                            secondaryParts.push(suggestion.context.region.name);
609                        }
610                        if (suggestion.context?.country?.name && !primary.includes(suggestion.context.country.name)) {
611                            secondaryParts.push(suggestion.context.country.name);
612                        }
613                        return {
614                            id: suggestion.mapbox_id || `mapbox-${index}`,
615                            label: suggestion.full_address || suggestion.name,
616                            ref: suggestion.mapbox_id,
617                            data: suggestion,
618                            primary,
619                            secondary: secondaryParts.join(', ')
620                        };
621                    });
622                },
623                arcgis: async function(query) {
624                    const params = new URLSearchParams({ text: query, f: 'json', maxSuggestions: 10 });
625                    if (this.countryRestriction) params.set('countryCode', this.countryRestriction);
626                    const response = await fetch(`https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest?${params}`);
627                    const data = await response.json();
628                    return (data.suggestions || []).map(suggestion => {
629                        const parts = suggestion.text.split(',').map(p => p.trim());
630                        return {
631                            id: suggestion.magicKey,
632                            label: suggestion.text,
633                            ref: suggestion.magicKey,
634                            primary: parts[0] || suggestion.text,
635                            secondary: parts.slice(1).join(', ')
636                        };
637                    });
638                },
639                photon: async function(query) {
640                    const url = new URL('https://photon.komoot.io/api/');
641                    url.searchParams.set('q', query);
642                    url.searchParams.set('limit', 10);
643                    if (this.geocodeLang) url.searchParams.set('lang', this.geocodeLang);
644                    const response = await fetch(url);
645                    const data = await response.json();
646                    const restrict = (this.countryRestriction || '').toUpperCase();
647                    const features = restrict
648                        ? (data.features || []).filter(f => ((f.properties || {}).countrycode || '').toUpperCase() === restrict)
649                        : (data.features || []);
650                    const skipTypes = ['yes', 'house', 'residential', 'building', 'address', 'apartments', 'detached', 'terrace'];
651                    return features.map((feature, index) => {
652                        const props = feature.properties || {};
653                        let primary = props.name || 'Unknown location';
654                        const secondaryParts = [];
655
656                        if (props.county && !primary.includes(props.county)) {
657                            primary = `${primary}, ${props.county}`;
658                        }
659
660                        const type = props.osm_value || props.type;
661                        if (type && !skipTypes.includes(type)) {
662                            secondaryParts.push(type);
663                        }
664
665                        if (props.state && !primary.includes(props.state)) {
666                            secondaryParts.push(props.state);
667                        }
668                        if (props.country && !primary.includes(props.country)) {
669                            secondaryParts.push(props.country);
670                        }
671
672                        const secondary = secondaryParts.join(', ');
673                        const labelParts = [props.name].filter(Boolean);
674                        if (props.state) labelParts.push(props.state);
675                        if (props.country) labelParts.push(props.country);
676                        return {
677                            id: `photon-${props.osm_id || ''}-${index}`,
678                            label: labelParts.join(', ') || primary,
679                            ref: feature,
680                            data: feature,
681                            primary,
682                            secondary
683                        };
684                    });
685                }
686            },
687
688            async _fetchPlaceDetailsNewAPI(suggestion) {
689                const place = new google.maps.places.Place({ id: suggestion.ref });
690                await place.fetchFields({ fields: ['displayName', 'formattedAddress', 'location'] });
691                return {
692                    formatted_address: place.formattedAddress,
693                    lat: place.location.lat(),
694                    lng: place.location.lng()
695                };
696            },
697
698            async _fetchPlaceDetailsLegacyAPI(suggestion) {
699                return new Promise(resolve => {
700                    this.placesService.getDetails({
701                        placeId: suggestion.ref,
702                        fields: ['formatted_address', 'geometry']
703                    }, (place, status) => {
704                        if (status === google.maps.places.PlacesServiceStatus.OK) {
705                            resolve({
706                                formatted_address: place.formatted_address,
707                                lat: place.geometry.location.lat(),
708                                lng: place.geometry.location.lng()
709                            });
710                            return;
711                        }
712                        resolve(null);
713                    });
714                });
715            },
716
717            placeDetailProviders: {
718                google: async function(suggestion) {
719                    const apiType = this.effectiveGoogleApiType();
720                    if (apiType === 'new') return this._fetchPlaceDetailsNewAPI(suggestion);
721                    if (apiType === 'legacy') return this._fetchPlaceDetailsLegacyAPI(suggestion);
722                    return null;
723                },
724                nominatim: async function(suggestion) {
725                    const item = suggestion.data;
726                    return {
727                        formatted_address: item.display_name,
728                        lat: item.lat,
729                        lng: item.lon
730                    };
731                },
732                mapbox: async function(suggestion) {
733                    const params = new URLSearchParams({
734                        access_token: this.mapboxToken,
735                        session_token: this.mapboxSessionToken || ''
736                    });
737                    if (this.geocodeLang && JR_MAPBOX_LANGUAGES.includes(this.geocodeLang)) params.set('language', this.geocodeLang);
738                    const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/retrieve/${suggestion.ref}?${params}`);
739                    const data = await response.json();
740                    const feature = data.features?.[0];
741                    if (!feature) return null;
742                    this.mapboxSessionToken = null;
743                    return {
744                        formatted_address: feature.properties?.full_address || feature.properties?.name || suggestion.label,
745                        lat: feature.geometry.coordinates[1],
746                        lng: feature.geometry.coordinates[0]
747                    };
748                },
749                arcgis: async function(suggestion) {
750                    const params = new URLSearchParams({ magicKey: suggestion.ref, f: 'json' });
751                    if (this.geocodeLang) params.set('langCode', this.geocodeLang);
752                    const response = await fetch(`https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates?${params}`);
753                    const data = await response.json();
754                    const candidate = data.candidates?.[0];
755                    if (!candidate) return null;
756                    return {
757                        formatted_address: candidate.address,
758                        lat: candidate.location.y,
759                        lng: candidate.location.x
760                    };
761                },
762                photon: async function(suggestion) {
763                    const feature = suggestion.data;
764                    return {
765                        formatted_address: suggestion.label,
766                        lat: feature.geometry.coordinates[1],
767                        lng: feature.geometry.coordinates[0]
768                    };
769                }
770            }
771        };
772    });
773}
774
775document.addEventListener('alpine:init', jrRegisterProximitySearch);
776jrRegisterProximitySearch();
777
778(() => {
779    window.jrFieldAdapters = window.jrFieldAdapters || [];
780
781    if (window.jrFieldAdapters.some(adapter => adapter && adapter.id === 'maps-proximity-geosearch')) {
782        return;
783    }
784
785    const fieldMappings = () => window.jrMapsAutocomplete?.fieldMappings || {};
786    const latField = () => fieldMappings().latitude || 'jr_latitude';
787    const lngField = () => fieldMappings().longitude || 'jr_longitude';
788    const clearNames = ['geosearch[address]', 'geosearch[lat]', 'geosearch[lng]', 'geosearch[radius]'];
789
790    const matchesFieldName = name => {
791        const value = String(name || '');
792        return value === 'geosearch' || value.startsWith('geosearch[');
793    };
794
795    const getRoot = (form, target = null) => {
796        return target?.closest?.('[data-jr-proximity-search]')
797            || form?.querySelector?.('[data-jr-proximity-search]')
798            || null;
799    };
800
801    const getData = root => {
802        if (!root) return null;
803        if (root._x_dataStack?.[0]) return root._x_dataStack[0];
804        if (typeof Alpine !== 'undefined' && typeof Alpine.$data === 'function') return Alpine.$data(root);
805        return null;
806    };
807
808    const clearNamedField = (form, name) => {
809        Array.from(form?.elements || [])
810            .filter(element => element.name === name)
811            .forEach(element => {
812                if (element.type === 'checkbox' || element.type === 'radio') {
813                    element.checked = false;
814                } else {
815                    element.value = '';
816                }
817            });
818    };
819
820    const resetComponentState = data => {
821        if (!data) return;
822        data.query = '';
823        data.lat = '';
824        data.lng = '';
825        data.radius = String(data.defaultRadius ?? data.radius ?? 30);
826        if (typeof data.closeSuggestions === 'function') data.closeSuggestions();
827    };
828
829    const clearKeys = () => {
830        const keys = [latField(), lngField(), 'jr_radius', 'location'];
831        const order = new URL(window.location.href).searchParams.get('order');
832        if (order === 'distance') keys.push('order');
833        return keys;
834    };
835
836    window.jrFieldAdapters.push({
837        id: 'maps-proximity-geosearch',
838        rootFieldName: 'geosearch',
839        matchesFieldName,
840        getCarryForwardKeys() {
841            return ['location'];
842        },
843        getClearKeys: clearKeys,
844        shouldHandleInput({ event, form }) {
845            if (event?.target?.name === 'geosearch[address]') return false;
846
847            const root = getRoot(form, event?.target);
848            if (root && root.contains(event?.target)) {
849                const latInput = root.querySelector('input[name="geosearch[lat]"]');
850                const lngInput = root.querySelector('input[name="geosearch[lng]"]');
851                if (!(latInput?.value && lngInput?.value)) return false;
852            }
853
854            return null;
855        },
856        shouldApplyChange({ event, form }) {
857            const root = getRoot(form, event?.target);
858            const latInput = root?.querySelector?.('input[name="geosearch[lat]"]') || form?.querySelector?.('input[name="geosearch[lat]"]');
859            const lngInput = root?.querySelector?.('input[name="geosearch[lng]"]') || form?.querySelector?.('input[name="geosearch[lng]"]');
860            const hasCoords = !!(latInput?.value && lngInput?.value);
861
862            if (event?.target?.name === 'geosearch[address]') {
863                const hasAddress = !!String(event.target.value || '').trim();
864                if (hasAddress && !hasCoords) return false;
865                return true;
866            }
867
868            if (root && root.contains(event?.target) && !hasCoords) {
869                return false;
870            }
871
872            return null;
873        },
874        clear(form, { target = null } = {}) {
875            const root = getRoot(form, target);
876            const data = getData(root);
877
878            if (data) {
879                resetComponentState(data);
880            }
881
882            clearNames.forEach(name => clearNamedField(form, name));
883
884            return clearKeys();
885        },
886        syncParams(params, { form, target = null } = {}) {
887            const root = getRoot(form, target);
888            const data = getData(root);
889            const addressInput = root?.querySelector?.('input[name="geosearch[address]"]') || form?.querySelector?.('input[name="geosearch[address]"]');
890            const latInput = root?.querySelector?.('input[name="geosearch[lat]"]') || form?.querySelector?.('input[name="geosearch[lat]"]');
891            const lngInput = root?.querySelector?.('input[name="geosearch[lng]"]') || form?.querySelector?.('input[name="geosearch[lng]"]');
892            const radiusInput = root?.querySelector?.('input[name="geosearch[radius]"]') || form?.querySelector?.('input[name="geosearch[radius]"]');
893
894            const address = String(data?.query ?? addressInput?.value ?? '').trim();
895            const lat = String(data?.lat ?? latInput?.value ?? '').trim();
896            const lng = String(data?.lng ?? lngInput?.value ?? '').trim();
897            const radius = String(data?.radius ?? radiusInput?.value ?? '').trim();
898
899            delete params[latField()];
900            delete params[lngField()];
901            delete params.jr_radius;
902            delete params.location;
903
904            if (!lat || !lng) {
905                if (String(params.order || '') === 'distance') {
906                    delete params.order;
907                }
908                return;
909            }
910
911            params[latField()] = lat;
912            params[lngField()] = lng;
913            params.jr_radius = radius || String(data?.defaultRadius ?? '');
914
915            if (address) {
916                params.location = address;
917            }
918
919            if (!params.order) {
920                params.order = 'distance';
921            }
922        },
923        async serialize(collected, { form, resolve = false } = {}) {
924            let address = (collected['geosearch[address]'] || [])[0] || '';
925            let lat = (collected['geosearch[lat]'] || [])[0] || '';
926            let lng = (collected['geosearch[lng]'] || [])[0] || '';
927            let radius = (collected['geosearch[radius]'] || [])[0] || '';
928
929            if (resolve && address && (!lat || !lng)) {
930                const root = getRoot(form);
931                const data = getData(root);
932
933                if (data?.ensureResolvedForSubmit) {
934                    await data.ensureResolvedForSubmit();
935                    address = data.query ? String(data.query) : '';
936                    lat = data.lat !== '' && data.lat != null ? String(data.lat) : '';
937                    lng = data.lng !== '' && data.lng != null ? String(data.lng) : '';
938                    radius = data.radius !== '' && data.radius != null ? String(data.radius) : radiu
938s;
939                }
940            }
941
942            delete collected['geosearch[address]'];
943            delete collected['geosearch[lat]'];
944            delete collected['geosearch[lng]'];
945            delete collected['geosearch[radius]'];
946
947            if (lat && lng) {
948                collected[latField()] = [lat];
949                collected[lngField()] = [lng];
950                if (radius) collected.jr_radius = [radius];
951                if (address) collected.location = [address];
952                if (!collected.order || !collected.order[0]) collected.order = ['distance'];
953            } else {
954                delete collected[latField()];
955                delete collected[lngField()];
956                delete collected.jr_radius;
957                delete collected.location;
958                if ((collected.order || [])[0] === 'distance') delete collected.order;
959            }
960
961            return collected;
962        },
963    });
964})();

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.