PageSourceSearch

https://services.unitedbaristas.com/wp-content/plugins/jreviews_ad…/js/location-search-client.js?v=1787990390

js unitedbaristas.com collected 2026-10-04 02:40:14 UTC 29,260 bytes, 594 lines download raw bytes

1/**
2 * Provider-neutral place search client.
3 *
4 * Provider responses are normalized to:
5 * { id, label, primary, secondary, lat, lng, bounds, components }
6 */
7(function (window) {
8    'use strict';
9
10    if (window.jrMapsLocationSearch) return;
11
12    const 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'];
13    const GENERIC_LOCATION_TYPES = new Set(['yes', 'house', 'residential', 'building', 'address', 'apartments', 'detached', 'terrace']);
14    const scriptPromises = new Map();
15    // A class being present in the weekly Maps JavaScript payload does not mean
16    // the key is entitled to call that API. Cache only a mode that completed a
17    // real autocomplete request successfully, scoped to the browser-visible key.
18    const verifiedGoogleApiTypes = new Map();
19
20    function loadScript(url) {
21        if (!url) return Promise.reject(new Error('A script URL is required.'));
22        if (scriptPromises.has(url)) return scriptPromises.get(url);
23
24        const promise = new Promise((resolve, reject) => {
25            const existing = document.querySelector(`script[src="${url}"]`);
26            if (existing) {
27                if (existing.dataset.jrLoaded === 'true') return resolve();
28                existing.addEventListener('load', resolve, { once: true });
29                existing.addEventListener('error', reject, { once: true });
30                return;
31            }
32
33            const script = document.createElement('script');
34            script.src = url;
35            script.async = true;
36            script.addEventListener('load', () => {
37                script.dataset.jrLoaded = 'true';
38                resolve();
39            }, { once: true });
40            script.addEventListener('error', reject, { once: true });
41            document.head.appendChild(script);
42        });
43
44        scriptPromises.set(url, promise);
45        return promise;
46    }
47
48    function uuid() {
49        return window.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
50    }
51
52    function parts(label) {
53        const values = String(label || '').split(',').map(value => value.trim()).filter(Boolean);
54        return { primary: values[0] || label || '', secondary: values.slice(1).join(', ') };
55    }
56
57    function countries(value) {
58        return String(value || '')
59            .split(',')
60            .map(country => country.trim().toLowerCase())
61            .filter(Boolean);
62    }
63
64    function suggestionLimit(value, fallback = 10) {
65        const limit = Number(value);
66        return Number.isInteger(limit) && limit > 0 ? Math.min(limit, 50) : fallback;
67    }
68
69    function photonType(properties) {
70        return properties.osm_value || properties.type || properties.osm_type || 'address';
71    }
72
73    function formatPhotonSuggestion(properties) {
74        const name = properties.name || 'Unknown location';
75        const county = properties.county;
76        const primary = county && !name.includes(county) ? `${name}, ${county}` : name;
77        const secondary = [];
78        const type = photonType(properties);
79
80        if (type && !GENERIC_LOCATION_TYPES.has(type)) secondary.push(type);
81        if (properties.state && !primary.includes(properties.state)) secondary.push(properties.state);
82        if (properties.country && !primary.includes(properties.country)) secondary.push(properties.country);
83
84        return {
85            primary,
86            secondary: secondary.join(', '),
87            label: [primary, ...secondary].join(', ')
88        };
89    }
90
91    function componentsFromGoogle(items, modern) {
92        const result = {};
93        (items || []).forEach(component => {
94            const long = modern ? component.longText : component.long_name;
95            const short = modern ? component.shortText : component.short_name;
96            if (component.types.includes('street_number')) result.streetNumber = long;
97            if (component.types.includes('route')) result.street = long;
98            if (component.types.includes('locality')) result.city = long;
99            if (!result.city && component.types.includes('postal_town')) result.city = long;
100            if (component.types.includes('administrative_area_level_1')) result.state = short || long;
101            if (component.types.includes('postal_code')) result.postalcode = long;
102            if (component.types.includes('country')) result.country = long;
103        });
104        result.address = [result.streetNumber, result.street].filter(
104Boolean).join(' ');
105        return result;
106    }
107
108    /**
109     * The provider did not answer, as distinct from answering with nothing.
110     *
111     * These are different states and must never share a message. A blocked,
112     * expired or over-quota token makes every request fail; reporting that as
113     * "no locations found" tells every visitor there is nothing near them and
114     * tells the owner nothing at all, while looking like an ordinary empty
115     * result. Callers show unavailability for this, and the honest empty state
116     * only for an answer that really was empty.
117     */
118    class LocationSearchUnavailable extends Error {
119        constructor(reason, status = 0, cause = null) {
120            super(`Location search provider did not answer (${reason}${status ? ' ' + status : ''})`);
121            this.name = 'LocationSearchUnavailable';
122            this.reason = reason;       // 'http' | 'network' | 'malformed'
123            this.status = status;       // HTTP status, 0 when there was no response
124            this.cause = cause;
125        }
126    }
127
128    /**
129     * Read a provider's JSON, refusing to turn a failure into an empty answer.
130     *
131     * Every branch here used to collapse into `{}`: `fetch(...).then(r => r.json())`
132     * never consulted `response.ok`, so a 403 body like {"message":"Forbidden"}
133     * parsed cleanly, the expected key was absent, and the caller received an
134     * empty list it could not tell from a real one.
135     */
136    async function fetchJson(url, options) {
137        let response;
138
139        try {
140            response = await fetch(url, options);
141        } catch (cause) {
142            // Network down, DNS failure, CORS rejection, aborted connection.
143            throw new LocationSearchUnavailable('network', 0, cause);
144        }
145
146        if (!response.ok) {
147            throw new LocationSearchUnavailable('http', response.status);
148        }
149
150        try {
151            return await response.json();
152        } catch (cause) {
153            // A 200 carrying HTML - a captive portal or an error page - is still
154            // the provider failing to answer, not an empty result.
155            throw new LocationSearchUnavailable('malformed', response.status, cause);
156        }
157    }
158
159    class LocationSearchClient {
160        constructor(config = {}) {
161            this.config = config;
162            this.provider = config.geocodingService || 'nominatim';
163            this.countryCodes = countries(config.countryRestriction);
164            this.limit = suggestionLimit(config.suggestionLimit);
165            this.mapboxSessionToken = null;
166            this.googleNewSessionToken = null;
167            this.googleLegacySessionToken = null;
168            this.googleModeKey = config.googleApiKey || '__loaded-google-api__';
169            this.googleApiType = verifiedGoogleApiTypes.get(this.googleModeKey) || null;
170            this.googleAutocomplete = null;
171            this.googlePlaces = null;
172        }
173
174        get supportsSuggestions() {
175            // The public Nominatim usage policy forbids autocomplete requests.
176            return this.provider !== 'nominatim';
177        }
178
179        async ensureReady() {
180            if (this.provider !== 'google') return;
181
182            // Legacy pages already load Google before constructing their provider.
183            // Do not require the modern coordinator in that case.
184            if (!window.google?.maps?.places) {
185                if (!window.jrMapsApi) await loadScript(this.config.coordinatorScript);
186                await window.jrMapsApi.loadGoogle(this.config.googleApiKey, {
187                    libraries: ['places'],
188                    locale: this.config.googleApiLocale || 'en',
189                    apiUrl: this.config.googleApiUrl
190                });
191            }
192
193            // The legacy services are built in ensureLegacyGoogleServices() at the
194            // moment a legacy call is made. Constructing them here made Google log
195            // its AutocompleteService deprecation notice on every page with a map,
196            // including the pages that go on to use the modern API exclusively.
197            const places = window.google?.maps?.places;
198            if (!places?.AutocompleteSuggestion?.fetchAutocompleteSuggestions && !places?.AutocompleteService) {
199                throw new Error('Google Places could not be loaded.');
200            }
201        }
202
203        /**
204         * Build the deprecated autocomplete/places services on first legacy use.
205         * Returns false when the payload cannot serve them.
206         */
207        ensureLegacyGoogleServices() {
208            const places = window.google?.maps?.places;
209            if (!places?.AutocompleteService) return false;
210
211            this.googleAutocomplete ||= new places.AutocompleteService();
212            this.googlePlaces ||= new places.PlacesService(document.createElement('div'));
213
214            return true;
215        }
216
217        async suggest(query, options = {}) {
218            const value = String(query || '').trim();
219            if (!value) return [];
220            if (!this.supportsSuggestions && !options.explicit) return [];
221            await this.ensureReady();
222            const provider = this[`suggest_${this.provider}`];
223            if (!provider) throw new Error(`Unsupported geocoder: ${this.provider}`);
224            return provider.call(this, value, options);
225        }
226
227        async resolve(suggestion) {
228            await this.ensureReady();
229            const provider = this[`resolve_${this.provider}`];
230            if (!provider) throw new Error(`Unsupported geocoder: ${this.provider}`);
231            const result = await provider.call(this, suggestion);
232            return result ? { ...suggestion, ...result, label: result.label || result.formatted_address || suggestion.label } : null;
233        }
234
235        async search(query) {
236            const suggestions = await this.suggest(query, { explicit: true });
237            return suggestions[0] ? this.resolve(suggestions[0]) : null;
238        }
239
240        verifyGoogleApiType(type) {
241            this.googleApiType = type;
242            verifiedGoogleApiTypes.set(this.googleModeKey, type);
243        }
244
245        isGoogleEntitlementError(error) {
246            const code = String(error?.code || error?.status || '').toLowerCase();
247            const message = `${code} ${String(error?.message || error || '')}`.toLowerCase();
248            if (Number(error?.code) === 7) return true;
249            return message.includes('request_denied')
250                || message.includes('permission_denied')
251                || message.includes('api_not_activated')
252                || message.includes('apinotactivatedmaperror')
253                || message.includes('requests to this api')
254                || message.includes('blocked')
255                || message.includes('legacy api')
256                || message.includes('not enabled')
257                || message.includes('disabled')
258                || message.includes('has not been used')
259                || message.includes('not authorized')
260                || message.includes('unauthorized')
261                || message.includes('permission')
262                || message.includes('access not configured')
263                || message.includes('forbidden')
264                || message.includes('403')
265                || message.includes('rpcerror');
266        }
267
268        async suggest_google(query) {
269            const places = google.maps.places;
270            const hasNew = Boolean(places?.AutocompleteSuggestion?.fetchAutocompleteSuggestions);
271            const hasLegacy = Boolean(places?.AutocompleteService);
272            const preferred = this.googleApiType;
273
274            if (preferred === 'legacy' && hasLegacy) {
275                try {
276                    return await this.suggestGoogleLegacy(query);
277                } catch (error) {
278                    if (!hasNew || !this.isGoogleEntitlementError(error)) throw error;
279                    this.googleLegacySessionToken = null;
280                    const suggestions = await this.suggestGoogleNew(query);
281                    this.verifyGoogleApiType('new');
282                    return suggestions;
283                }
284            }
285
286            if (hasNew) {
287                try {
288                    const suggestions = await this.suggestGoogleNew(query);
289                    this.verifyGoogleApiType('new');
290                    return suggestions;
291                } catch (error) {
292                    if (!hasLegacy || !this.isGoogleEntitlementError(error)) throw error;
293                    this.googleNewSessionToken = null;
294                    const suggestions = await this.suggestGoogleLegacy(query);
295                    this.verifyGoogleApiType('legacy');
296                    return suggestions;
297                }
298            }
299
300            if (hasLegacy) {
301                const suggestions = await this.suggestGoogleLegacy(query);
302                this.verifyGoogleApiType('legacy');
303                return suggestions;
304            }
305
306            throw new Error('No compatible Google Places autocomplete API is available.');
307        }
308
309        async suggestGoogleNew(query) {
310            const places = google.maps.places;
311            this.googleNewSessionToken ||= new places.AutocompleteSessionToken();
312            const request = { input: query, sessionToken: this.googleNewSessionToken };
313            if (this.countryCodes.length) request.includedRegionCodes = this.countryCodes;
314            const response = await places.AutocompleteSuggestion.fetchAutocompleteSuggestions(request);
315            return (response.suggestions || []).filter(item => item.placePrediction).map(item => {
316                const prediction = item.placePrediction;
317                return {
318                    id: prediction.placeId,
319                    ref: prediction.placeId,
320                    data: prediction,
321                    apiGeneration: 'new',
322                    label: prediction.text?.text || '',
323                    primary: prediction.structuredFormat?.mainText?.text || prediction.text?.text || '',
324                    secondary: prediction.structuredFormat?.secondaryText?.text || ''
325                };
326            });
327        }
328
329        suggestGoogleLegacy(query) {
330            const places = google.maps.places;
331            if (!this.ensureLegacyGoogleServices()) {
332                return Promise.reject(new Error('Google Places legacy autocomplete is not available.'));
333            }
334            this.googleLegacySessionToken ||= new places.AutocompleteSessionToken();
335            return new Promise((resolve, reject) => {
336                const request = { input: query, sessionToken: this.googleLegacySessionToken };
337                if (this.countryCodes.length) request.componentRestrictions = { country: this.countryCodes };
338                this.googleAutocomplete.getPlacePredictions(request, (predictions, status) => {
339                    if (status === places.PlacesServiceStatus.ZERO_RESULTS) return resolve([]);
340                    if (status !== places.PlacesServiceStatus.OK || !predictions) {
341                        return reject(new Error(`Google Places legacy autocomplete failed: ${status || 'UNKNOWN_ERROR'}`));
342                    }
343                    resolve(predictions.map(prediction => ({
344                        id: prediction.place_id,
345                        ref: prediction.place_id,
346                        data: prediction,
347                        googleSessionToken: this.googleLegacySessionToken,
348                        apiGeneration: 'legacy',
349                        label: prediction.description,
350                        primary: prediction.structured_formatting?.main_text || prediction.description,
351                        secondary: prediction.structured_formatting?.secondary_text || ''
352                    })));
353                });
354            });
355        }
356
357        async resolve_google(suggestion) {
358            const generation = suggestion.apiGeneration || this.googleApiType;
359            if (generation === 'new') {
360                if (suggestion.apiGeneration === 'new' && !suggestion.data?.toPlace) {
361                    throw new Error('Google Places New suggestion is missing its native PlacePrediction.');
362                }
363                const place = suggestion.data?.toPlace ? suggestion.data.toPlace() : new google.maps.places.Place({ id: suggestion.ref });
364                try {
365                    await place.fetchFields({ fields: ['formattedAddress', 'addressComponents', 'location', 'viewport'] });
366                    const viewport = place.viewport;
367                    return {
368                        formatted_address: place.formattedAddress,
369                        lat: place.location.lat(),
370                        lng: place.location.lng(),
371                        bounds: viewport ? [viewport.getSouthWest
371().lng(), viewport.getSouthWest().lat(), viewport.getNorthEast().lng(), viewport.getNorthEast().lat()] : null,
372                        components: componentsFromGoogle(place.addressComponents, true)
373                    };
374                } finally {
375                    this.googleNewSessionToken = null;
376                }
377            }
378
379            if (generation !== 'legacy') throw new Error('Google suggestion API generation is unknown.');
380            if (!this.ensureLegacyGoogleServices()) {
381                throw new Error('Google Places legacy details are not available.');
382            }
383            return new Promise((resolve, reject) => {
384                this.googlePlaces.getDetails({
385                    placeId: suggestion.ref,
386                    sessionToken: suggestion.googleSessionToken || this.googleLegacySessionToken,
387                    fields: ['formatted_address', 'address_components', 'geometry']
388                }, (place, status) => {
389                    this.googleLegacySessionToken = null;
390                    if (status !== google.maps.places.PlacesServiceStatus.OK || !place) {
391                        return reject(new Error(`Google Places legacy details failed: ${status || 'UNKNOWN_ERROR'}`));
392                    }
393                    const viewport = place.geometry.viewport;
394                    resolve({
395                        formatted_address: place.formatted_address,
396                        lat: place.geometry.location.lat(),
397                        lng: place.geometry.location.lng(),
398                        bounds: viewport ? [viewport.getSouthWest
398().lng(), viewport.getSouthWest().lat(), viewport.getNorthEast().lng(), viewport.getNorthEast().lat()] : null,
399                        components: componentsFromGoogle(place.address_components, false)
400                    });
401                });
402            });
403        }
404
405        async suggest_mapbox(query) {
406            this.mapboxSessionToken ||= uuid();
407            const params = new URLSearchParams({
408                q: query,
409                access_token: this.config.mapboxToken || '',
410                session_token: this.mapboxSessionToken,
411                limit: String(this.limit)
412            });
413            if (this.countryCodes.length) params.set('country', this.countryCodes.join(','));
414            if (MAPBOX_LANGUAGES.includes(this.config.geocodeLang)) params.set('language', this.config.geocodeLang);
415            const data = await fetchJson(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
416            return (data.suggestions || []).map((item, index) => {
417                const place = item.context?.place?.name;
418                let primary = item.name || item.full_address;
419                if (place && !primary.includes(place)) primary = `${primary}, ${place}`;
420                return {
421                    id: item.mapbox_id || `mapbox-${index}`,
422                    ref: item.mapbox_id,
423                    data: item,
424                    label: item.full_address || item.name,
425                    primary,
426                    secondary: [item.poi_category?.[0] || (item.feature_type !== 'address' ? item.feature_type : ''), item.context?.region?.name, item.context?.country?.name].filter(Boolean).join(', ')
427                };
428            });
429        }
430
431        async resolve_mapbox(suggestion) {
432            const params = new URLSearchParams({ access_token: this.config.mapboxToken || '', session_token: this.mapboxSessionToken || '' });
433            if (MAPBOX_LANGUAGES.includes(this.config.geocodeLang)) params.set('language', this.config.geocodeLang);
434            const data = await fetchJson(`https://api.mapbox.com/search/searchbox/v1/retrieve/${encodeURIComponent(suggestion.ref)}?${params}`);
435            this.mapboxSessionToken = null;
436            const feature = data.features?.[0];
437            if (!feature) return null;
438            const properties = feature.properties || {};
439            return {
440                formatted_address: properties.full_address || properties.name,
441                lat: feature.geometry.coordinates[1],
442                lng: feature.geometry.coordinates[0],
443                bounds: feature.bbox || null,
444                components: {
445                    city: properties.context?.place?.name,
446                    state: properties.context?.region?.name,
447                    postalcode: properties.context?.postcode?.name,
448                    country: properties.context?.country?.name
449                }
450            };
451        }
452
453        async suggest_nominatim(query) {
454            const url = new URL('https://nominatim.openstreetmap.org/search');
455            url.searchParams.set('q', query);
456            url.searchParams.set('format', 'json');
457            url.searchParams.set('limit', '1');
458            url.searchParams.set('addressdetails', '1');
459            if (this.countryCodes.length) url.searchParams.set('countrycodes', this.countryCodes.join(','));
460            if (this.config.geocodeLang) url.searchParams.set('accept-language', this.config.geocodeLang);
461            const data = await fetchJson(url);
462            return data.map(item => ({ id: item.place_id, ref: item, data: item, label: item.display_name, ...parts(item.display_name) }));
463        }
464
465        resolve_nominatim(suggestion) {
466            const item = suggestion.data;
467            const box = item.boundingbox?.map(Number);
468            return {
469                formatted_address: item.display_name,
470                lat: Number(item.lat),
471                lng: Number(item.lon),
472                bounds: box?.length === 4 ? [box[2], box[0], box[3], box[1]] : null,
473                components: { city: item.address?.city || item.address?.town || item.address?.village, state: item.address?.state, postalcode: item.address?.postcode, country: item.address?.country }
474            };
475        }
476
477        async suggest_arcgis(query) {
478            const params = new URLSearchParams({ text: query, f: 'json', maxSuggestions: String(this.limit) });
479            if (this.countryCodes.length) params.set('countryCode', this.countryCodes.join(','));
480            const data = await fetchJson(`https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest?${params}`);
481            return (data.suggestions || []).map(item => ({ id: item.magicKey, ref: item.magicKey, label: item.text, ...parts(item.text) }));
482        }
483
484        async resolve_arcgis(suggestion) {
485            const params = new URLSearchParams({ magicKey: suggestion.ref, f: 'json', outFields: 'City,Region,Postal,Country' });
486            if (this.config.geocodeLang) params.set('langCode', this.config.geocodeLang);
487            const data = await fetchJson(`https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates?${params}`);
488            const item = data.candidates?.[0];
489            if (!item) return null;
490            return { formatted_address: item.address, lat: item.location.y, lng: item.location.x, bounds: item.extent ? [item.extent.xmin, item.extent.ymin, item.extent.xmax, item.extent.ymax] : null, components: { city: item.attributes.City, state: item.attributes.Region, postalcode: item.attributes.Postal, country: item.attributes.Country } };
491        }
492
493        async suggest_photon(query) {
494            const url = new URL('https://photon.komoot.io/api/');
495            url.searchParams.set('q', query);
496            // Photon has no country parameter. Fetch a wider candidate set when a
497            // country restriction is configured, then apply the setting locally.
498            url.searchParams.set('limit', String(this.countryCodes.length ? Math.min(Math.max(this.limit * 5, 10), 50) : this.limit));
499            if (this.config.geocodeLang) url.searchParams.set('lang', this.config.geocodeLang);
500            const data = await fetchJson(url);
501            return (data.features || []).filter(feature => {
502                if (!this.countryCodes.length) return true;
503                return this.countryCodes.includes(String(feature.properties?.countrycode || '').toLowerCase());
504            }).slice(0, this.limit).map((feature, index) => {
505                const properties = feature.properties || {};
506                const formatted = formatPhotonSuggestion(properties);
507                // Photon can return the same OSM object more than once with a
508                // different key/value (Golden Gate Bridge is a real example).
509                // Include every discriminator so Alpine x-for keys stay unique.
510                const id = ['photon', properties.osm_type, properties.osm_id, properties.osm_key, properties.osm_value, index]
511                    .filter(value => value !== undefined && value !== null && value !== '')
512                    .join('-');
513                return { id, ref: feature, data: feature, ...formatted };
514            });
515        }
516
517        resolve_photon(suggestion) {
518            const feature = suggestion.data;
519            const properties = feature.properties || {};
520            return {
521                formatted_address: suggestion.label,
522                lat: feature.geometry.coordinates[1],
523                lng: feature.geometry.coordinates[0],
524                bounds: feature.bbox || null,
525                type: photonType(properties),
526                components: {
527                    address: [properties.housenumber, properties.street].filter(Boolean).join(' '),
528                    city: properties.city || properties.town || properties.village,
529                    county: properties.county,
530                    state: properties.state,
531                    postalcode: properties.postcode,
532                    country: properties.country,
533                    countrycode: properties.countrycode
534                }
535            };
536        }
537    }
538
539    /** One report per distinct failure per page, not one per keystroke. */
540    const reportedFailures = new Set();
541
542    /**
543     * Tell the site owner their geocoder is failing.
544     *
545     * The visitor already sees an honest message; this is the other half, and
546     * the half that was missing entirely. A referrer-restricted or expired token
547     * fails in the BROWSER, so without this the server never learns of it and
548     * the owner sees a feature that appears to work.
549     *
550     * Best effort by design: reporting must never break, block or slow the
551     * search it is reporting on, so every failure here is swallowed.
552     */
553    function reportUnavailable(error, provider) {
554        if (error?.name !== 'LocationSearchUnavailable') return;
555
556        const url = window.jrMapsAutocomplete?.reportUrl;
557        if (!url) return;
558
559        const reason = error.reason || 'network';
560        const status = Number(error.status) || 0;
561        const key = `${provider}|${reason}|${status}`;
562
563        if (reportedFailures.has(key)) return;
564        reportedFailures.add(key);
565
566        const body = new URLSearchParams({
567            provider: String(provider || ''),
568            reason: String(reason),
569            status: String(status)
570        });
571
572        try {
573            if (navigator.sendBeacon && navigator.sendBeacon(url, body)) return;
574
575            fetch(url, {
576                method: 'POST',
577                body,
578                keepalive: true,
579                headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
580            }).catch(() => {});
581        } catch (ignored) {
582            // Never let telemetry surface as a visitor-facing failure.
583        }
584    }
585
586    window.jrMapsLocationSearch = {
587        create(config) { return new LocationSearchClient(config); },
588        report: reportUnavailable,
589        // Published so a consumer can tell "did not answer" from "answered with
590        // nothing" without matching on message text.
591        Unavailable: LocationSearchUnavailable,
592        isUnavailable(error) { return error instanceof LocationSearchUnavailable; }
593    };
594})(window);

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.