@fieldia/map 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -3
- package/package.json +4 -4
- package/src/lib/address-labels.d.mts +14 -2
- package/src/lib/address-labels.d.ts +14 -2
- package/src/lib/address-labels.js.map +1 -1
- package/src/lib/address-labels.mjs.map +1 -1
- package/src/lib/address-line.d.mts +10 -1
- package/src/lib/address-line.d.ts +10 -1
- package/src/lib/address-line.js +25 -8
- package/src/lib/address-line.js.map +1 -1
- package/src/lib/address-line.mjs +24 -8
- package/src/lib/address-line.mjs.map +1 -1
- package/src/lib/address-names.d.mts +38 -0
- package/src/lib/address-names.d.ts +38 -0
- package/src/lib/address-names.js +240 -0
- package/src/lib/address-names.js.map +1 -0
- package/src/lib/address-names.mjs +232 -0
- package/src/lib/address-names.mjs.map +1 -0
- package/src/lib/address-parts.d.mts +128 -10
- package/src/lib/address-parts.d.ts +128 -10
- package/src/lib/address-parts.js +836 -17
- package/src/lib/address-parts.js.map +1 -1
- package/src/lib/address-parts.mjs +827 -14
- package/src/lib/address-parts.mjs.map +1 -1
- package/src/lib/address-search.d.mts +8 -4
- package/src/lib/address-search.d.ts +8 -4
- package/src/lib/address-search.js +27 -9
- package/src/lib/address-search.js.map +1 -1
- package/src/lib/address-search.mjs +28 -10
- package/src/lib/address-search.mjs.map +1 -1
- package/src/lib/address-suggest.d.mts +5 -2
- package/src/lib/address-suggest.d.ts +5 -2
- package/src/lib/address-suggest.js +161 -28
- package/src/lib/address-suggest.js.map +1 -1
- package/src/lib/address-suggest.mjs +161 -28
- package/src/lib/address-suggest.mjs.map +1 -1
- package/src/lib/form-map.js +9 -7
- package/src/lib/form-map.js.map +1 -1
- package/src/lib/form-map.mjs +10 -8
- package/src/lib/form-map.mjs.map +1 -1
- package/src/lib/place.js +21 -10
- package/src/lib/place.js.map +1 -1
- package/src/lib/place.mjs +21 -10
- package/src/lib/place.mjs.map +1 -1
- package/src/lib/providers.d.mts +6 -0
- package/src/lib/providers.d.ts +6 -0
- package/src/lib/providers.js +46 -97
- package/src/lib/providers.js.map +1 -1
- package/src/lib/providers.mjs +46 -97
- package/src/lib/providers.mjs.map +1 -1
- package/src/lib/route-line.d.mts +9 -0
- package/src/lib/route-line.d.ts +9 -0
- package/src/lib/route-line.js +13 -0
- package/src/lib/route-line.js.map +1 -1
- package/src/lib/route-line.mjs +11 -0
- package/src/lib/route-line.mjs.map +1 -1
- package/src/lib/routes.js +6 -2
- package/src/lib/routes.js.map +1 -1
- package/src/lib/routes.mjs +6 -2
- package/src/lib/routes.mjs.map +1 -1
- package/src/lib/search-box.d.mts +15 -3
- package/src/lib/search-box.d.ts +15 -3
- package/src/lib/search-box.js +118 -34
- package/src/lib/search-box.js.map +1 -1
- package/src/lib/search-box.mjs +118 -34
- package/src/lib/search-box.mjs.map +1 -1
package/src/lib/address-parts.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.googleParts = exports.inCountries = exports.mapboxParts = exports.nominatimParts = exports.photonParts = void 0;
|
|
4
|
+
exports.knownCountries = knownCountries;
|
|
5
|
+
exports.googleRegions = googleRegions;
|
|
6
|
+
exports.azurePlaces = azurePlaces;
|
|
7
|
+
exports.azureNamed = azureNamed;
|
|
8
|
+
exports.azureSearch = azureSearch;
|
|
9
|
+
exports.azureLocate = azureLocate;
|
|
10
|
+
exports.googlePlaces = googlePlaces;
|
|
4
11
|
const answers_1 = require("./answers");
|
|
12
|
+
const address_line_1 = require("./address-line");
|
|
13
|
+
const address_names_1 = require("./address-names");
|
|
14
|
+
const map_core_1 = require("./map-core");
|
|
5
15
|
/** Words without the direction marks Azure and Google put in Arabic. */
|
|
6
16
|
const text = (value) => (typeof value === 'string' ? value.replace(/[--]/g, '').trim() : '');
|
|
7
|
-
/** The parts there are, the
|
|
8
|
-
function partsOf(street, number, city, region, postcode, country) {
|
|
17
|
+
/** The parts there are, the country's code in capitals; nothing when none is. */
|
|
18
|
+
function partsOf({ street, number, numberFirst, city, region, postcode, country }) {
|
|
19
|
+
const road = text(street);
|
|
20
|
+
const house = road && text(number);
|
|
9
21
|
const found = {
|
|
10
|
-
street:
|
|
22
|
+
street: house ? (numberFirst ? `${house} ${road}` : `${road} ${house}`) : road,
|
|
11
23
|
city: text(city),
|
|
12
24
|
region: text(region),
|
|
13
25
|
postcode: text(postcode),
|
|
@@ -20,28 +32,835 @@ function partsOf(street, number, city, region, postcode, country) {
|
|
|
20
32
|
const photonParts = (p) => {
|
|
21
33
|
const kind = (0, answers_1.pathOf)(p, 'type');
|
|
22
34
|
const own = (as) => (kind === as ? (0, answers_1.pathOf)(p, 'name') : undefined);
|
|
23
|
-
return partsOf((0, answers_1.pathOf)(p, 'street') ?? own('street'), (0, answers_1.pathOf)(p, 'housenumber'), (0, answers_1.pathOf)(p, 'city') ?? own('city'), (0, answers_1.pathOf)(p, 'state'), (0, answers_1.pathOf)(p, 'postcode'), (0, answers_1.pathOf)(p, 'countrycode'));
|
|
35
|
+
return partsOf({ street: (0, answers_1.pathOf)(p, 'street') ?? own('street'), number: (0, answers_1.pathOf)(p, 'housenumber'), city: (0, answers_1.pathOf)(p, 'city') ?? own('city'), region: (0, answers_1.pathOf)(p, 'state'), postcode: (0, answers_1.pathOf)(p, 'postcode'), country: (0, answers_1.pathOf)(p, 'countrycode') });
|
|
24
36
|
};
|
|
25
37
|
exports.photonParts = photonParts;
|
|
26
38
|
/** Nominatim's `address` (with `addressdetails=1`): a town or a village where there is no city. */
|
|
27
|
-
const nominatimParts = (a) => partsOf((0, answers_1.pathOf)(a, 'road'), (0, answers_1.pathOf)(a, 'house_number'), (0, answers_1.pathOf)(a, 'city') ?? (0, answers_1.pathOf)(a, 'town') ?? (0, answers_1.pathOf)(a, 'village'), (0, answers_1.pathOf)(a, 'state'), (0, answers_1.pathOf)(a, 'postcode'), (0, answers_1.pathOf)(a, 'country_code'));
|
|
39
|
+
const nominatimParts = (a) => partsOf({ street: (0, answers_1.pathOf)(a, 'road'), number: (0, answers_1.pathOf)(a, 'house_number'), city: (0, answers_1.pathOf)(a, 'city') ?? (0, answers_1.pathOf)(a, 'town') ?? (0, answers_1.pathOf)(a, 'village'), region: (0, answers_1.pathOf)(a, 'state'), postcode: (0, answers_1.pathOf)(a, 'postcode'), country: (0, answers_1.pathOf)(a, 'country_code') });
|
|
28
40
|
exports.nominatimParts = nominatimParts;
|
|
29
|
-
/**
|
|
30
|
-
const azureParts = (a) => partsOf(text((0, answers_1.pathOf)(a, 'addressLine')) || (0, answers_1.pathOf)(a, 'streetName'), text((0, answers_1.pathOf)(a, 'addressLine')) ? '' : (0, answers_1.pathOf)(a, 'streetNumber'), (0, answers_1.pathOf)(a, 'locality'), (0, answers_1.pathOf)(a, 'adminDistricts')?.map((d) => text((0, answers_1.pathOf)(d, 'name')) || (0, answers_1.pathOf)(d, 'shortName'))[0], (0, answers_1.pathOf)(a, 'postalCode'), (0, answers_1.pathOf)(a, 'countryRegion', 'ISO') ?? (0, answers_1.pathOf)(a, 'countryRegion', 'iso'));
|
|
31
|
-
exports.azureParts = azureParts;
|
|
32
|
-
/** Google's `addressComponents`, each by its type: the country by its short code. */
|
|
33
|
-
const googleParts = (components) => {
|
|
34
|
-
const of = (type, short) => (0, answers_1.pathOf)((Array.isArray(components) ? components : []).find((c) => (0, answers_1.pathOf)(c, 'types')?.includes(type)), short ? 'shortText' : 'longText');
|
|
35
|
-
return partsOf(of('route'), of('street_number'), of('locality') ?? of('postal_town'), of('administrative_area_level_1'), of('postal_code'), of('country', true));
|
|
36
|
-
};
|
|
37
|
-
exports.googleParts = googleParts;
|
|
38
|
-
/** Mapbox's `context`: its address's own line (number and street), or the street alone. */
|
|
41
|
+
/** Mapbox's `context`: its address's own line (number and street, in its country's order), or the street alone. */
|
|
39
42
|
const mapboxParts = (p) => {
|
|
40
43
|
const at = (...path) => (0, answers_1.pathOf)(p, 'context', ...path);
|
|
41
|
-
return partsOf(at('address', 'name') ?? at('street', 'name'),
|
|
44
|
+
return partsOf({ street: at('address', 'name') ?? at('street', 'name'), city: at('place', 'name') ?? at('locality', 'name'), region: at('region', 'name'), postcode: at('postcode', 'name'), country: at('country', 'country_code') });
|
|
42
45
|
};
|
|
43
46
|
exports.mapboxParts = mapboxParts;
|
|
44
47
|
/** Only places in the countries asked for, when some are: for a provider that cannot be asked to keep to them. */
|
|
45
48
|
const inCountries = (places, countries) => countries?.length ? places.filter((place) => countries.includes(place.parts?.country ?? '')) : places;
|
|
46
49
|
exports.inCountries = inCountries;
|
|
50
|
+
const regionNames = (locale) => (typeof Intl.DisplayNames === 'function' ? new Intl.DisplayNames([locale], { type: 'region', fallback: 'none' }) : null);
|
|
51
|
+
/**
|
|
52
|
+
* The countries a provider can be asked to keep to: ISO codes, each once,
|
|
53
|
+
* "UK" read as "GB" (as every provider's parts name it). A code no one
|
|
54
|
+
* knows ("XX", "EGY") is left out and said once: Azure and Google refuse a
|
|
55
|
+
* whole search for one.
|
|
56
|
+
*/
|
|
57
|
+
function knownCountries(countries) {
|
|
58
|
+
const names = regionNames('en');
|
|
59
|
+
const codes = [];
|
|
60
|
+
for (const given of countries ?? []) {
|
|
61
|
+
const code = String(given).trim().toUpperCase().replace(/^UK$/, 'GB');
|
|
62
|
+
let known = /^[A-Z]{2}$/.test(code);
|
|
63
|
+
try {
|
|
64
|
+
known &&= !names || !!names.of(code);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
known = false;
|
|
68
|
+
}
|
|
69
|
+
if (!known)
|
|
70
|
+
(0, map_core_1.warnOnce)(`"${given}" is not a country's ISO code ("EG", "SA"): the address search leaves it out.`);
|
|
71
|
+
else if (!codes.includes(code))
|
|
72
|
+
codes.push(code);
|
|
73
|
+
}
|
|
74
|
+
return codes;
|
|
75
|
+
}
|
|
76
|
+
/** The countries Google is asked to keep to (`includedRegionCodes`): 15 at most, as it refuses a whole search for more; with more, none, said once. */
|
|
77
|
+
function googleRegions(countries) {
|
|
78
|
+
const codes = knownCountries(countries);
|
|
79
|
+
if (codes.length <= 15)
|
|
80
|
+
return codes.map((code) => code.toLowerCase());
|
|
81
|
+
(0, map_core_1.warnOnce)(`Google keeps its suggestions to 15 countries at most: with ${codes.length}, none are asked for, and its suggestions come from anywhere.`);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
/** A country named in the page's language ("مصر", "Egypt"), or as the provider wrote it where the browser names none. */
|
|
85
|
+
function countryName(code, locale, own) {
|
|
86
|
+
try {
|
|
87
|
+
return (/^[A-Z]{2}$/.test(code) && regionNames(locale)?.of(code)) || text(own);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return text(own);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ---- Azure Maps --------------------------------------------------------------------
|
|
94
|
+
const sameWords = (one, other) => !!(0, address_names_1.nameKey)(one) && (0, address_names_1.nameKey)(one) === (0, address_names_1.nameKey)(other);
|
|
95
|
+
/** An admin district's code ("RIY", "DU"), as Azure gives a province's or an emirate's beside its name. */
|
|
96
|
+
const isCode = (name) => /^[A-Z]{2,3}$/.test(name);
|
|
97
|
+
const isoOf = (a) => (text((0, answers_1.pathOf)(a, 'countryRegion', 'ISO')) || text((0, answers_1.pathOf)(a, 'countryRegion', 'iso'))).toUpperCase();
|
|
98
|
+
/** An address's admin districts, coarsest first, each by every name Azure gives it: its suggestions give a name and a code ("Riyadh", "RIY"), its geocoding one of them. */
|
|
99
|
+
function districtsOf(a) {
|
|
100
|
+
const listed = (0, answers_1.pathOf)(a, 'adminDistricts');
|
|
101
|
+
return (Array.isArray(listed) ? listed : []).map((d) => [text((0, answers_1.pathOf)(d, 'name')), text((0, answers_1.pathOf)(d, 'shortName'))].filter(Boolean));
|
|
102
|
+
}
|
|
103
|
+
const nameOf = (names = []) => names.find((name) => !isCode(name)) ?? '';
|
|
104
|
+
const STREETS = ['Address', 'RoadBlock', 'RoadIntersection'];
|
|
105
|
+
const AREAS = ['PopulatedPlace', 'Neighborhood', 'AdminDivision1', 'AdminDivision2', 'AdminDivision3', 'CountryRegion', 'Postcode1'];
|
|
106
|
+
const levelOf = (type) => (STREETS.includes(String(type)) ? 'street' : AREAS.includes(String(type)) ? 'area' : 'named');
|
|
107
|
+
/** A suggestion's level: a road with no name of its own reads as the area it is in ("العليا", Riyadh), and is that. */
|
|
108
|
+
const levelOfPlace = (type, street) => (levelOf(type) === 'street' && !street ? 'area' : levelOf(type));
|
|
109
|
+
/** A region's name without the word for its kind, where it is not one Fieldia knows ("Ajman Emirate" is "Ajman", "إمارة دبي" "دبي"). */
|
|
110
|
+
const KIND = /^(?:إمارة|امارة)\s+|\s+emirate$/iu;
|
|
111
|
+
const withoutKind = (name) => name.replace(KIND, '');
|
|
112
|
+
/** The emirates that are a city and its quarters, by Azure's codes: their quarters' city is the emirate's. */
|
|
113
|
+
const CITY_EMIRATES = ['DU', 'AJ', 'UQ'];
|
|
114
|
+
/**
|
|
115
|
+
* Egypt's governorates that are a city and its districts, by the names
|
|
116
|
+
* Fieldia writes for them: a district Azure files under one alone ("El
|
|
117
|
+
* Zamalek", Cairo) is the governorate's city's; another governorate's
|
|
118
|
+
* locality is its own town (Mansoura, in Dakahlia; Kharga, in New Valley).
|
|
119
|
+
*/
|
|
120
|
+
const CITY_GOVERNORATES = ['Cairo', 'Giza', 'Alexandria', 'Port Said', 'Suez'];
|
|
121
|
+
/**
|
|
122
|
+
* Egypt's governorate of Helwan (2008 to 2011), Cairo's again since: Azure
|
|
123
|
+
* still files Cairo's southern quarters under it ("Qesm El Maadi", Cairo,
|
|
124
|
+
* Helwan), which are Cairo's, as their addresses say. Helwan itself is a town.
|
|
125
|
+
*/
|
|
126
|
+
const FORMER_GOVERNORATES = { EG: ['helwan', 'حلوان'] };
|
|
127
|
+
/**
|
|
128
|
+
* The cities of the emirate of Abu Dhabi, whose quarters Azure files under
|
|
129
|
+
* the emirate alone ("Al Khalidiyah", "Al Jimi"): a quarter is in the city
|
|
130
|
+
* it is within reach of (km), as its place found says; elsewhere (Al
|
|
131
|
+
* Dhafra's towns) it is its own town.
|
|
132
|
+
*/
|
|
133
|
+
const ABU_DHABI_CITIES = [
|
|
134
|
+
{ lat: 24.4539, lng: 54.3773, km: 45, name: 'Abu Dhabi' },
|
|
135
|
+
{ lat: 24.2075, lng: 55.7447, km: 30, name: 'Al Ain' },
|
|
136
|
+
];
|
|
137
|
+
const kmBetween = (a, b) => {
|
|
138
|
+
const rad = Math.PI / 180;
|
|
139
|
+
const h = Math.sin(((b.lat - a.lat) * rad) / 2) ** 2 + Math.cos(a.lat * rad) * Math.cos(b.lat * rad) * Math.sin(((b.lng - a.lng) * rad) / 2) ** 2;
|
|
140
|
+
return 12742 * Math.asin(Math.sqrt(h));
|
|
141
|
+
};
|
|
142
|
+
/** A city's or region's name as Fieldia writes it in the page's language, where it knows the place ("Al Jizah" is "Giza", "Ad Dammām" "Dammam"); as Azure wrote it otherwise. */
|
|
143
|
+
function cityName(country, name, locale) {
|
|
144
|
+
const known = (0, address_names_1.knownCity)(country, name) ?? (0, address_names_1.knownRegion)(country, name);
|
|
145
|
+
return known ? (0, address_names_1.nameIn)(known, locale) : name;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* An address of Azure's where it is: its place (its locality's first
|
|
149
|
+
* piece, "Zamâlek, Cairo"'s "Zamâlek"), its city, region and country.
|
|
150
|
+
*
|
|
151
|
+
* Its region is its first admin district, known by Azure's code for it or
|
|
152
|
+
* one of its names ("SH", "Ash Shāriqah", "Sharjah Emirate" are Sharjah),
|
|
153
|
+
* and named one way in each language; one Fieldia does not know is named as
|
|
154
|
+
* Azure names it, the word for an emirate aside.
|
|
155
|
+
*
|
|
156
|
+
* Its admin districts say the city, each country's own way: in Egypt the
|
|
157
|
+
* second (a city, "New Cairo", "6th of October"); where there is none, its
|
|
158
|
+
* locality where that is a city Fieldia knows ("Ţanţā", "Sheikh Zayed
|
|
159
|
+
* City"), else the city of a governorate that is one (Zamalek and Nasr City
|
|
160
|
+
* are Cairo's, Dokki Giza's: the locality there is a district), else the
|
|
161
|
+
* locality, a town ("El Kharga", in New Valley); in Saudi Arabia the second,
|
|
162
|
+
* a governorate named for its city, or the place itself, a town, where there
|
|
163
|
+
* is none ("Jeddah", in Makkah); in Jordan the first, a governorate named
|
|
164
|
+
* for its city; in the UAE the emirate where it is a city and its quarters,
|
|
165
|
+
* its locality (a town, "Kalba") in the others, and with no emirate named
|
|
166
|
+
* only a locality Fieldia knows for a city ("البرشاء 2" is none: its
|
|
167
|
+
* geocoding names its emirate). Elsewhere its locality is the city. A city
|
|
168
|
+
* Fieldia knows is named one way in each language too.
|
|
169
|
+
*/
|
|
170
|
+
function azureWhere(a, locale) {
|
|
171
|
+
const country = isoOf(a);
|
|
172
|
+
const districts = districtsOf(a);
|
|
173
|
+
const known = (0, address_names_1.knownRegion)(country, ...(districts[0] ?? []));
|
|
174
|
+
const region = known ? (0, address_names_1.nameIn)(known, locale) : withoutKind(nameOf(districts[0]));
|
|
175
|
+
const second = nameOf(districts[1]);
|
|
176
|
+
const pieces = text((0, answers_1.pathOf)(a, 'locality'))
|
|
177
|
+
.split(',')
|
|
178
|
+
.map((piece) => piece.trim())
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
const place = pieces[0] ?? '';
|
|
181
|
+
const town = pieces[pieces.length - 1] ?? '';
|
|
182
|
+
const isCity = (name) => ((0, address_names_1.knownCity)(country, name) ? name : '');
|
|
183
|
+
const former = (FORMER_GOVERNORATES[country] ?? []).includes((0, address_names_1.nameKey)(second)) && !sameWords(second, place);
|
|
184
|
+
const cities = {
|
|
185
|
+
EG: (!former && second) || isCity(town) || (known && CITY_GOVERNORATES.includes(known.names.en) ? region : '') || town || region,
|
|
186
|
+
SA: second || town || region,
|
|
187
|
+
JO: region || town,
|
|
188
|
+
AE: !known ? isCity(town) : known.code && CITY_EMIRATES.includes(known.code) ? region : town || region,
|
|
189
|
+
};
|
|
190
|
+
const city = cityName(country, cities[country] ?? town, locale);
|
|
191
|
+
return { place, pieces, country, region, city, abuDhabi: known?.code === 'AZ' };
|
|
192
|
+
}
|
|
193
|
+
/** The city of a place found in the emirate of Abu Dhabi, by where it is; elsewhere its city as its address says. */
|
|
194
|
+
function cityAt(a, at, locale) {
|
|
195
|
+
const where = azureWhere(a, locale);
|
|
196
|
+
const near = where.abuDhabi ? ABU_DHABI_CITIES.find((city) => kmBetween(city, at) <= city.km) : undefined;
|
|
197
|
+
return near ? cityName('AE', near.name, locale) : where.city;
|
|
198
|
+
}
|
|
199
|
+
/** The names an address's own fields give its places: its neighbourhood, locality, admin districts (and their codes), postcode and country. */
|
|
200
|
+
function namedIn(a) {
|
|
201
|
+
return [
|
|
202
|
+
(0, answers_1.pathOf)(a, 'neighborhood'),
|
|
203
|
+
...text((0, answers_1.pathOf)(a, 'locality')).split(','),
|
|
204
|
+
...districtsOf(a).flat(),
|
|
205
|
+
(0, answers_1.pathOf)(a, 'postalCode'),
|
|
206
|
+
(0, answers_1.pathOf)(a, 'countryRegion', 'name'),
|
|
207
|
+
isoOf(a),
|
|
208
|
+
]
|
|
209
|
+
.map(address_names_1.nameKey)
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The districts Azure's own line names between a street and its locality,
|
|
214
|
+
* which its fields leave out ("30 Street, Al Fisht, Sharjah", whose
|
|
215
|
+
* neighbourhood is none and locality Sharjah: Al Fisht): the street's two
|
|
216
|
+
* of a number told apart. Only between the street and a place its fields
|
|
217
|
+
* name; none where its line is written another way.
|
|
218
|
+
*/
|
|
219
|
+
function districtsInLine(a, street) {
|
|
220
|
+
const formatted = text((0, answers_1.pathOf)(a, 'formattedAddress'));
|
|
221
|
+
if (!street || !formatted.startsWith(street))
|
|
222
|
+
return [];
|
|
223
|
+
const named = namedIn(a);
|
|
224
|
+
const found = [];
|
|
225
|
+
for (const piece of formatted
|
|
226
|
+
.slice(street.length)
|
|
227
|
+
.split(/\s+[-–]\s+|[,،]/)
|
|
228
|
+
.map((one) => one.trim())
|
|
229
|
+
.filter(Boolean)) {
|
|
230
|
+
if (named.includes((0, address_names_1.nameKey)(piece)))
|
|
231
|
+
return found;
|
|
232
|
+
found.push(piece);
|
|
233
|
+
}
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Pieces of a line, each place said once: one that names a city or region
|
|
238
|
+
* Fieldia knows, said in the page's own words elsewhere in the line (`own`,
|
|
239
|
+
* its city and region) or above it (`before`), is left out ("Cairo" on an
|
|
240
|
+
* Arabic page beside "القاهرة"; "Al Jizah" beside "Giza"), as one that is
|
|
241
|
+
* the same words but for their accents is ("Ţanţā" beside "Tanta").
|
|
242
|
+
*/
|
|
243
|
+
function onceKnown(country, pieces, own, before) {
|
|
244
|
+
const said = before.map(text).filter(Boolean);
|
|
245
|
+
const kept = [];
|
|
246
|
+
for (const piece of pieces.map(text)) {
|
|
247
|
+
if (!piece)
|
|
248
|
+
continue;
|
|
249
|
+
const other = (name) => ((0, address_names_1.nameKey)(name) !== (0, address_names_1.nameKey)(piece) && (0, address_names_1.sameKnown)(country, piece, name)) || (name !== piece && (0, address_names_1.nameKey)(name) === (0, address_names_1.nameKey)(piece));
|
|
250
|
+
if (!own.includes(piece) && own.some(other))
|
|
251
|
+
continue;
|
|
252
|
+
if (said.some(other))
|
|
253
|
+
continue;
|
|
254
|
+
said.push(piece);
|
|
255
|
+
kept.push(piece);
|
|
256
|
+
}
|
|
257
|
+
return kept;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* An address of Azure's on one line from its own fields, in order (`first`,
|
|
261
|
+
* then its neighbourhood, the districts its line names between, its
|
|
262
|
+
* locality, city, region, postcode and country), each place said once (the
|
|
263
|
+
* same words, or the names of one city or region), the country named in the
|
|
264
|
+
* page's language: never its formatted address's emirate codes ("DU"),
|
|
265
|
+
* spellings ("Mişr") and dashes ("King Fahd Road - Al Olaya").
|
|
266
|
+
*/
|
|
267
|
+
function azureLine(a, locale, first, before = [], street = '') {
|
|
268
|
+
const { pieces, city, region, country } = azureWhere(a, locale);
|
|
269
|
+
const line = [first, (0, answers_1.pathOf)(a, 'neighborhood'), ...districtsInLine(a, street), ...pieces, city, region, (0, answers_1.pathOf)(a, 'postalCode'), countryName(country, locale, (0, answers_1.pathOf)(a, 'countryRegion', 'name'))];
|
|
270
|
+
return (0, address_line_1.addressLine)(onceKnown(country, line, [city, region], before), locale, before);
|
|
271
|
+
}
|
|
272
|
+
/** Digits as numbers, Arabic's too ("٢٦" is "26"). */
|
|
273
|
+
const asDigits = (words) => words.replace(/[٠-٩۰-۹]/g, (d) => String(d.charCodeAt(0) % 16));
|
|
274
|
+
const numbersIn = (words) => asDigits(words).match(/\d+/g) ?? [];
|
|
275
|
+
/** The words for a street, a road, a lane and a number, which a street named by its number alone has beside it ("30 Street", "شارع 6", "Street No. 8", "طريق رقم 9"). */
|
|
276
|
+
const STREET_WORDS = new Set(['street', 'st', 'road', 'rd', 'lane', 'ln', 'no', 'number', 'شارع', 'طريق', 'رقم', 'سكة', 'ممر', 'حارة']);
|
|
277
|
+
/**
|
|
278
|
+
* A street named by its number alone ("30 Street", "8", "شارع 6"): each
|
|
279
|
+
* district of a town in the UAE, Saudi Arabia and Egypt numbers its own, so
|
|
280
|
+
* one of the number in another district is another street.
|
|
281
|
+
*/
|
|
282
|
+
const isNumbered = (street) => numbersIn(street).length > 0 &&
|
|
283
|
+
asDigits(street)
|
|
284
|
+
.toLowerCase()
|
|
285
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
286
|
+
.filter(Boolean)
|
|
287
|
+
.every((word) => /^\d+$/.test(word) || STREET_WORDS.has(word));
|
|
288
|
+
/**
|
|
289
|
+
* A street line of Azure's up to the first place its fields name: its
|
|
290
|
+
* street line may carry them after it ("Bldg. 13 Bay Square, Business Bay,
|
|
291
|
+
* Dubai, Uae", in Business Bay).
|
|
292
|
+
*/
|
|
293
|
+
function ownLine(a, line) {
|
|
294
|
+
const pieces = line.split(/[,،]/).map((piece) => piece.trim());
|
|
295
|
+
const named = namedIn(a);
|
|
296
|
+
const at = pieces.findIndex((piece, i) => i > 0 && named.includes((0, address_names_1.nameKey)(piece)));
|
|
297
|
+
return at > 0 ? pieces.slice(0, at).join(', ') : line;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Azure's street line, without a house number it made up: a street whose
|
|
301
|
+
* name has a number ("26 July Street", "شارع 26 يوليو", typed "شارع ٢٦
|
|
302
|
+
* يوليو") comes with that number as a house's too, unless it was typed
|
|
303
|
+
* again, for a house. Azure's whole line where it differs from its parts
|
|
304
|
+
* ("15 El Tahrir Street").
|
|
305
|
+
*/
|
|
306
|
+
function azureStreet(a, typed) {
|
|
307
|
+
const line = ownLine(a, text((0, answers_1.pathOf)(a, 'addressLine')));
|
|
308
|
+
const number = asDigits(text((0, answers_1.pathOf)(a, 'streetNumber')));
|
|
309
|
+
const street = ownLine(a, text((0, answers_1.pathOf)(a, 'streetName')));
|
|
310
|
+
if (!number || !street)
|
|
311
|
+
return line;
|
|
312
|
+
const inName = numbersIn(street).filter((n) => n === number).length;
|
|
313
|
+
return inName && numbersIn(typed).filter((n) => n === number).length <= inName ? street : line;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The place a road with no name of its own is in, where its fields name
|
|
317
|
+
* none below its town and its line does: the first of its line's pieces
|
|
318
|
+
* before the places its fields name ("Abu Tina, Charjah, Émirats arabes
|
|
319
|
+
* unis", whose locality is Charjah: Abu Tina).
|
|
320
|
+
*/
|
|
321
|
+
function areaInLine(a) {
|
|
322
|
+
const named = namedIn(a);
|
|
323
|
+
const [first] = text((0, answers_1.pathOf)(a, 'formattedAddress'))
|
|
324
|
+
.split(/\s+[-–]\s+|[,،]/)
|
|
325
|
+
.map((piece) => piece.trim())
|
|
326
|
+
.filter(Boolean);
|
|
327
|
+
return first && !named.includes((0, address_names_1.nameKey)(first)) ? first : '';
|
|
328
|
+
}
|
|
329
|
+
/** Azure's places on two lines: the street, or the place's own name (a quarter's, a town's), then where it is; their parts when asked. */
|
|
330
|
+
function azurePlaces(found, locale, typed = '', withParts, near) {
|
|
331
|
+
const features = (0, answers_1.pathOf)(found, 'features');
|
|
332
|
+
if (!Array.isArray(features))
|
|
333
|
+
return [];
|
|
334
|
+
return features.flatMap((feature) => {
|
|
335
|
+
const properties = (0, answers_1.pathOf)(feature, 'properties');
|
|
336
|
+
const a = (0, answers_1.pathOf)(properties, 'address');
|
|
337
|
+
const type = (0, answers_1.pathOf)(properties, 'type');
|
|
338
|
+
const street = levelOf(type) === 'street' ? azureStreet(a, typed) : '';
|
|
339
|
+
const level = levelOfPlace(type, street);
|
|
340
|
+
const where = azureWhere(a, locale);
|
|
341
|
+
// A quarter's or a town's own name: its neighbourhood, a nameless road's the place its line names it in, or its locality's first ("Zamâlek, Cairo", whose formatted address starts with Cairo); a town Fieldia knows by the name it writes for it ("Ad Dammām" is Dammam, "Charjah" Sharjah).
|
|
342
|
+
const area = level === 'area' ? text((0, answers_1.pathOf)(a, 'neighborhood')) || (levelOf(type) === 'street' && areaInLine(a)) || where.place : '';
|
|
343
|
+
const own = area && (0, address_names_1.sameKnown)(where.country, area, where.city) ? where.city : area;
|
|
344
|
+
const label = street || text((0, answers_1.pathOf)(properties, 'name')) || own || text((0, answers_1.pathOf)(a, 'formattedAddress')).split(',')[0].trim();
|
|
345
|
+
if (!label)
|
|
346
|
+
return [];
|
|
347
|
+
const detail = azureLine(a, locale, '', [label], text((0, answers_1.pathOf)(a, 'addressLine')));
|
|
348
|
+
const [lng, lat] = (0, answers_1.pathOf)(feature, 'geometry', 'coordinates') ?? [];
|
|
349
|
+
const parts = withParts ? partsOf({ street, city: where.city, region: where.region, postcode: (0, answers_1.pathOf)(a, 'postalCode'), country: where.country }) : undefined;
|
|
350
|
+
return [
|
|
351
|
+
{
|
|
352
|
+
label,
|
|
353
|
+
...(detail ? { detail } : {}),
|
|
354
|
+
...(typeof lat === 'number' && typeof lng === 'number' ? { lat, lng } : {}),
|
|
355
|
+
...(parts ? { parts } : {}),
|
|
356
|
+
country: where.country,
|
|
357
|
+
azure: { address: a, level, street, ...(level === 'area' ? { area: area || label } : {}), type: String(type), ...(near ? { near } : {}) },
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** A point named by Azure: its first address, in order. */
|
|
363
|
+
function azureNamed(found, locale) {
|
|
364
|
+
const features = (0, answers_1.pathOf)(found, 'features');
|
|
365
|
+
const a = Array.isArray(features) ? (0, answers_1.pathOf)(features[0], 'properties', 'address') : undefined;
|
|
366
|
+
const line = text((0, answers_1.pathOf)(a, 'addressLine'));
|
|
367
|
+
return azureLine(a, locale, ownLine(a, line), [], line) || (0, address_line_1.addressLine)([(0, answers_1.pathOf)(a, 'formattedAddress')], locale);
|
|
368
|
+
}
|
|
369
|
+
/** The whole world, for an autocomplete with no place to look near: Azure answers none without a box or a point. */
|
|
370
|
+
const WORLD = '-180,-90,180,90';
|
|
371
|
+
/** The most countries asked for one at a time; with more, Azure is asked once and its answer kept to them. */
|
|
372
|
+
const ONE_AT_A_TIME = 10;
|
|
373
|
+
/**
|
|
374
|
+
* Every kind of place Azure's autocomplete names (api-version 2026-01-01)
|
|
375
|
+
* but a postcode alone ("56967, Saudi Arabia"), as an address field asks for
|
|
376
|
+
* them: with the whole world to look in, a town's postcodes crowd out its
|
|
377
|
+
* districts. Azure refuses a name it does not know, so these are its own.
|
|
378
|
+
*/
|
|
379
|
+
const NOT_POSTCODES = 'Address AdminDivision1 AdminDivision2 AdminDivision3 AdministrativeBuilding Airport AirportTerminal AmusementPark AncientSite Aquarium Archipelago Basin Battlefield Bay Beach Bridge BusinessStructure Camp Canal Casino Castle Cave Cemetery Channel Church Cliff Coast CommunityCenter Continent CountryRegion Crater CulturalRegion Dam Delta Desert DrainageBasin Dune FacilityStructure FerryTerminal FinancialStructure FireStation Forest Fort Garden Geyser Glacier GolfCourse GreaterPopulatedPlace HigherEducationFacility HistoricalSite Hospital HotSpring Ice IndigenousPeoplesReserve IndustrialStructure Island Isthmus Lake LandArea Landform LandmarkBuilding Library Lighthouse Marina Market MedicalStructure MetroStation MilitaryBase Mission Monument Mosque Mountain MountainRange MultiModalStation Museum NauticalStructure Neighborhood Oasis ObservationPoint Ocean OfficeBuilding Other Park ParkingStructure Pass Peninsula Plain Planet Plateau Playground PlayingField Pole PoliticalUnit PopulatedPlace Port Promontory RaceTrack RailwayStation Reef ReligiousStructure Reserve ResidentialStructure River RoadBlock Ruin ScientificResearchBase Sea SeaplaneLandingArea Shrine SkiArea Spring Stadium StatisticalDistrict Temple Theater TouristRailway TouristStructure Trailhead TransportationStructure Tunnel Valley Volcano Walkway Wall Waterfall WaterFeature Wetland Winery Zoo'
|
|
380
|
+
.split(' ')
|
|
381
|
+
.join(',');
|
|
382
|
+
/** The word for a district typed before its name ("حي الملقا", "حى الزمالك"), which Azure's names of districts leave out. */
|
|
383
|
+
const DISTRICT_WORD = /^\s*(?:حي|حى)\s+(?=\S)/u;
|
|
384
|
+
/** A row's second line, by its pieces. */
|
|
385
|
+
const piecesOf = (detail = '') => detail.split(/[,،]/).map((piece) => piece.trim()).filter(Boolean);
|
|
386
|
+
/**
|
|
387
|
+
* Whether a row says less of the same place than another: an area of the
|
|
388
|
+
* same name, every piece of its line in the other's, which says only a
|
|
389
|
+
* postcode more (Tanta's division, beside the town with its postcode), or
|
|
390
|
+
* anything more where this one's suggestion names no region (Azure's
|
|
391
|
+
* "البرشاء 2" with no emirate, beside its "البرشاء 2" in Dubai). One that
|
|
392
|
+
* names a town more is another place ("Al Rawdah | Al Kharj, Riyadh" beside
|
|
393
|
+
* Riyadh's own); two rows of a street are two of its stretches.
|
|
394
|
+
*/
|
|
395
|
+
function saysLess(place, other) {
|
|
396
|
+
const [mine, theirs] = [piecesOf(place.detail), piecesOf(other.detail)];
|
|
397
|
+
const more = theirs.filter((piece) => !mine.includes(piece));
|
|
398
|
+
return (other !== place &&
|
|
399
|
+
place.azure.level === 'area' &&
|
|
400
|
+
other.azure.level === 'area' &&
|
|
401
|
+
other.label === place.label &&
|
|
402
|
+
more.length > 0 &&
|
|
403
|
+
mine.every((piece) => theirs.includes(piece)) &&
|
|
404
|
+
(more.every((piece) => /^\p{Nd}[\p{Nd}\s-]*$/u.test(piece)) || !districtsOf(place.azure.address).length));
|
|
405
|
+
}
|
|
406
|
+
/** The same two lines, said once, where the first of them is; a row that says less of the same place than another, as that one, where it is. */
|
|
407
|
+
function onceEach(places) {
|
|
408
|
+
const kept = [];
|
|
409
|
+
for (const place of places) {
|
|
410
|
+
const one = places.find((other) => saysLess(place, other)) ?? place;
|
|
411
|
+
if (!kept.some((other) => other === one || (other.label === one.label && other.detail === one.detail)))
|
|
412
|
+
kept.push(one);
|
|
413
|
+
}
|
|
414
|
+
return kept;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Azure's suggestions: nearest the place the page gives first, or the whole
|
|
418
|
+
* world's by prominence when it gives none; kept to the countries the page
|
|
419
|
+
* names by Azure itself (`countryRegion`), one request for each (a tenth of
|
|
420
|
+
* a Search transaction each; a list of them answers nothing), taken in
|
|
421
|
+
* turn, five in all. For an address field (`addresses`), no postcodes alone,
|
|
422
|
+
* and the addresses and areas first, the places with names of their own
|
|
423
|
+
* (a station, a park) after them, as Azure's types say. A row that reads as
|
|
424
|
+
* one before it is left out, as one that says less of the same place than
|
|
425
|
+
* another is. The word for a district typed before its name ("حي الروضة")
|
|
426
|
+
* is left out of the words asked, as Azure names its districts without it
|
|
427
|
+
* (with it, Azure lists other towns' streets and districts named with the
|
|
428
|
+
* word, and none of Riyadh's Al Rawdah); the words are asked as typed only
|
|
429
|
+
* when that finds nothing.
|
|
430
|
+
*/
|
|
431
|
+
async function azureSearch(autocomplete, request) {
|
|
432
|
+
const district = request.text.match(DISTRICT_WORD);
|
|
433
|
+
if (district) {
|
|
434
|
+
const found = await azureSearch(autocomplete, { ...request, text: request.text.slice(district[0].length) });
|
|
435
|
+
if (found.length)
|
|
436
|
+
return found;
|
|
437
|
+
}
|
|
438
|
+
return azureSearchFor(autocomplete, request);
|
|
439
|
+
}
|
|
440
|
+
async function azureSearchFor(autocomplete, request) {
|
|
441
|
+
const { text: typed, near, locale, countries, parts, addresses } = request;
|
|
442
|
+
const codes = knownCountries(countries);
|
|
443
|
+
if (codes.length > ONE_AT_A_TIME)
|
|
444
|
+
(0, map_core_1.warnOnce)(`Azure Maps is asked for ${ONE_AT_A_TIME} countries at most, one at a time: with ${codes.length}, its suggestions are kept to them after it answers, and may be few.`);
|
|
445
|
+
const where = { ...(near ? { coordinates: `${near.lng},${near.lat}` } : { bbox: WORLD }), ...(addresses ? { resultTypes: NOT_POSTCODES } : {}) };
|
|
446
|
+
const each = codes.length && codes.length <= ONE_AT_A_TIME ? codes : [undefined];
|
|
447
|
+
const answers = await Promise.allSettled(each.map((code) => autocomplete({ query: typed, ...where, countryRegion: code })));
|
|
448
|
+
// One country's refusal leaves the others' suggestions; every one's is the search's.
|
|
449
|
+
const refused = answers.find((answer) => answer.status === 'rejected');
|
|
450
|
+
if (refused && answers.every((answer) => answer.status === 'rejected'))
|
|
451
|
+
throw refused.reason;
|
|
452
|
+
const lists = answers.map((answer) => (answer.status === 'fulfilled' ? azurePlaces(answer.value, locale, typed, parts, near) : []));
|
|
453
|
+
const taken = [];
|
|
454
|
+
for (let i = 0; lists.some((list) => i < list.length); i++)
|
|
455
|
+
taken.push(...lists.flatMap((list) => list.slice(i, i + 1)));
|
|
456
|
+
const kept = onceEach(taken.filter((place) => !codes.length || codes.includes(place.country)));
|
|
457
|
+
const named = (place) => place.azure.level === 'named';
|
|
458
|
+
return (addresses ? [...kept.filter((place) => !named(place)), ...kept.filter(named)] : kept).slice(0, 5);
|
|
459
|
+
}
|
|
460
|
+
/** A geocoding hit's address. */
|
|
461
|
+
const addressOf = (hit) => (0, answers_1.pathOf)(hit, 'properties', 'address');
|
|
462
|
+
/**
|
|
463
|
+
* Whether a geocoding hit is in the suggestion's own country and admin
|
|
464
|
+
* districts, each as Azure names it at its level: a region Fieldia knows by
|
|
465
|
+
* which it is (its code or any of its names: "SH" and "Ash Shāriqah" are
|
|
466
|
+
* one), another by its codes, or by the same words where neither side has
|
|
467
|
+
* one, or the names of one city ("Al Jizah", "Giza"). "Al Rawdah" in
|
|
468
|
+
* Riyadh's governorate, never in Al Wasiah's.
|
|
469
|
+
*/
|
|
470
|
+
function fits(hit, a) {
|
|
471
|
+
const theirs = addressOf(hit);
|
|
472
|
+
const country = isoOf(a);
|
|
473
|
+
if (country && isoOf(theirs) && country !== isoOf(theirs))
|
|
474
|
+
return false;
|
|
475
|
+
const levels = districtsOf(theirs);
|
|
476
|
+
return districtsOf(a).every((ours, i) => {
|
|
477
|
+
const their = levels[i] ?? [];
|
|
478
|
+
if (!ours.length || !their.length)
|
|
479
|
+
return true;
|
|
480
|
+
if (i === 0) {
|
|
481
|
+
const [mine, other] = [(0, address_names_1.knownRegion)(country, ...ours), (0, address_names_1.knownRegion)(country, ...their)];
|
|
482
|
+
if (mine && other)
|
|
483
|
+
return mine === other;
|
|
484
|
+
}
|
|
485
|
+
// Codes with codes ("RIY"); names only where neither side has a code, as Azure spells a coded one differently in each service.
|
|
486
|
+
const [codes, theirCodes] = [ours.filter(isCode), their.filter(isCode)];
|
|
487
|
+
if (codes.length && theirCodes.length)
|
|
488
|
+
return codes.some((code) => theirCodes.includes(code));
|
|
489
|
+
if (codes.length || theirCodes.length)
|
|
490
|
+
return true;
|
|
491
|
+
return ours.some((name) => their.some((other) => sameWords(name, other) || (0, address_names_1.sameKnown)(country, name, other)));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Whether a hit is the place itself, as Azure says: at the suggestion's
|
|
496
|
+
* level (a street for a street, a town or district for one, a named place
|
|
497
|
+
* for one), without Azure's own doubt (`UpHierarchy`: found only above it,
|
|
498
|
+
* as a street for a house; `Low` confidence). A district's centre for a
|
|
499
|
+
* street, or a town's for its district, is only near it.
|
|
500
|
+
*/
|
|
501
|
+
function isItself(hit, level) {
|
|
502
|
+
const properties = (0, answers_1.pathOf)(hit, 'properties');
|
|
503
|
+
const codes = (0, answers_1.pathOf)(properties, 'matchCodes');
|
|
504
|
+
if ((Array.isArray(codes) && codes.includes('UpHierarchy')) || (0, answers_1.pathOf)(properties, 'confidence') === 'Low')
|
|
505
|
+
return false;
|
|
506
|
+
const type = (0, answers_1.pathOf)(properties, 'type');
|
|
507
|
+
if (level === 'street')
|
|
508
|
+
return !!text((0, answers_1.pathOf)(addressOf(hit), 'addressLine')) || !!text((0, answers_1.pathOf)(addressOf(hit), 'streetName'));
|
|
509
|
+
if (level === 'area')
|
|
510
|
+
return levelOf(type) === 'area' && type !== 'Postcode1' && type !== 'CountryRegion';
|
|
511
|
+
return levelOf(type) === 'named';
|
|
512
|
+
}
|
|
513
|
+
/** A hit's own name: its street line, its neighbourhood, or its locality's first piece. */
|
|
514
|
+
const ownName = (hit) => {
|
|
515
|
+
const a = addressOf(hit);
|
|
516
|
+
return text((0, answers_1.pathOf)(a, 'addressLine')) || text((0, answers_1.pathOf)(a, 'neighborhood')) || text((0, answers_1.pathOf)(a, 'locality')).split(',')[0].trim();
|
|
517
|
+
};
|
|
518
|
+
const pointOf = (hit) => {
|
|
519
|
+
const [lng, lat] = (0, answers_1.pathOf)(hit, 'geometry', 'coordinates');
|
|
520
|
+
return { lat, lng };
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
* Of the places of one kind and name among a geocoding's hits (Azure cannot
|
|
524
|
+
* tell Abu Dhabi's Al Bateen from Al Ain's, and marks them `Ambiguous`, or
|
|
525
|
+
* not, as its two forms answer), the one nearest where the search looked:
|
|
526
|
+
* the suggestion was found nearest it.
|
|
527
|
+
*/
|
|
528
|
+
function nearestOf(first, among, near) {
|
|
529
|
+
const kind = (0, answers_1.pathOf)(first, 'properties', 'type');
|
|
530
|
+
const twins = among.filter((one) => (0, answers_1.pathOf)(one, 'properties', 'type') === kind && sameWords(ownName(one), ownName(first)));
|
|
531
|
+
return near && twins.length > 1 ? twins.reduce((best, one) => (kmBetween(near, pointOf(one)) < kmBetween(near, pointOf(best)) ? one : best)) : first;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* A street suggestion's own district, where its address names one below its
|
|
535
|
+
* town: its neighbourhood ("Al Mina"), the district its line names after it
|
|
536
|
+
* ("30 Street, Al Fisht, Sharjah"), or its locality where that is not its
|
|
537
|
+
* city (Cairo's "El Balaqsa", Riyadh's "Al Rawdah").
|
|
538
|
+
*/
|
|
539
|
+
function districtOf(a, street, locale) {
|
|
540
|
+
const [inLine] = districtsInLine(a, street);
|
|
541
|
+
const { place, city, country } = azureWhere(a, locale);
|
|
542
|
+
return text((0, answers_1.pathOf)(a, 'neighborhood')) || inLine || (place && !(0, address_names_1.sameKnown)(country, place, city) ? place : '');
|
|
543
|
+
}
|
|
544
|
+
/** Whether a geocoding hit names a district among its places: its neighbourhood, its locality's pieces, its line's. */
|
|
545
|
+
function names(hit, district) {
|
|
546
|
+
const a = addressOf(hit);
|
|
547
|
+
const key = (0, address_names_1.nameKey)(district);
|
|
548
|
+
return [(0, answers_1.pathOf)(a, 'neighborhood'), ...text((0, answers_1.pathOf)(a, 'locality')).split(','), ...text((0, answers_1.pathOf)(a, 'formattedAddress')).split(/\s+[-–]\s+|[,،]/)].some((piece) => (0, address_names_1.nameKey)(piece) === key);
|
|
549
|
+
}
|
|
550
|
+
/** Whether a geocoding hit is in another district than `district`: it names one of its own (its neighbourhood, or its line's after its street) and not this one. */
|
|
551
|
+
function elsewhere(hit, district) {
|
|
552
|
+
const a = addressOf(hit);
|
|
553
|
+
return !names(hit, district) && !!(text((0, answers_1.pathOf)(a, 'neighborhood')) || districtsInLine(a, text((0, answers_1.pathOf)(a, 'addressLine')))[0]);
|
|
554
|
+
}
|
|
555
|
+
/** Whether Azure found a hit only above what was asked of it (`UpHierarchy`: the street for a house, the district for a street). */
|
|
556
|
+
const isAbove = (hit) => {
|
|
557
|
+
const codes = (0, answers_1.pathOf)(hit, 'properties', 'matchCodes');
|
|
558
|
+
return Array.isArray(codes) && codes.includes('UpHierarchy');
|
|
559
|
+
};
|
|
560
|
+
/** Whether a hit is a street (a street's line, or a house on one), as Azure names it. */
|
|
561
|
+
const isStreet = (hit) => !!text((0, answers_1.pathOf)(addressOf(hit), 'addressLine')) || !!text((0, answers_1.pathOf)(addressOf(hit), 'streetName'));
|
|
562
|
+
/** Whether Azure found several places for the words asked (`Ambiguous`), this one among them: which is meant, only the suggestion's own district tells. */
|
|
563
|
+
const isAmbiguous = (hit) => {
|
|
564
|
+
const codes = (0, answers_1.pathOf)(hit, 'properties', 'matchCodes');
|
|
565
|
+
return Array.isArray(codes) && codes.includes('Ambiguous');
|
|
566
|
+
};
|
|
567
|
+
/**
|
|
568
|
+
* How far beyond a district's box (Azure's `bbox` for it) a street's hit is
|
|
569
|
+
* still its own: a road with a name runs on through the districts beside it
|
|
570
|
+
* (King Fahd Road, filed in Al Rahmaniyah, is found 0.6 km beyond its box,
|
|
571
|
+
* in the King Fahd district), where the street of a number is another
|
|
572
|
+
* district's own ("10 41 Street" is Al Ghuwair's as well as Abu Tina's).
|
|
573
|
+
*/
|
|
574
|
+
const BEYOND_KM = { named: 1.5, numbered: 0.25 };
|
|
575
|
+
/** How near a district's point its own street's hit is, where Azure gives no box for the district. */
|
|
576
|
+
const NEAR_KM = 1.5;
|
|
577
|
+
/** How far a hit is beyond an area found: from its box; from its point, less a district's reach, where Azure gives no box. */
|
|
578
|
+
function beyond(hit, area) {
|
|
579
|
+
const at = pointOf(hit);
|
|
580
|
+
const box = (0, answers_1.pathOf)(area, 'bbox');
|
|
581
|
+
if (!Array.isArray(box) || box.length !== 4 || !box.every((n) => typeof n === 'number'))
|
|
582
|
+
return Math.max(0, kmBetween(at, pointOf(area)) - NEAR_KM);
|
|
583
|
+
const [west, south, east, north] = box;
|
|
584
|
+
return kmBetween(at, { lat: Math.min(Math.max(at.lat, south), north), lng: Math.min(Math.max(at.lng, west), east) });
|
|
585
|
+
}
|
|
586
|
+
/** An area of the town's size or less: never a province's, a country's or a postcode's middle, tens or hundreds of kilometres off. */
|
|
587
|
+
const isSmall = (hit) => levelOf((0, answers_1.pathOf)(hit, 'properties', 'type')) === 'area' && !['AdminDivision1', 'CountryRegion', 'Postcode1'].includes(String((0, answers_1.pathOf)(hit, 'properties', 'type')));
|
|
588
|
+
/**
|
|
589
|
+
* Where a suggestion of Azure's is (its suggestions carry no place), asked
|
|
590
|
+
* of its geocoding, the hits in the suggestion's own country and admin
|
|
591
|
+
* districts kept (`fits`; "Al Rawdah" in Riyadh's governorate, never in
|
|
592
|
+
* Al Wasiah's).
|
|
593
|
+
*
|
|
594
|
+
* A street, by its own address in Azure's structured form: its street line
|
|
595
|
+
* (never with a house number Azure made up), its own district for its
|
|
596
|
+
* locality where it names one below its town (its neighbourhood, the
|
|
597
|
+
* district its line names, "Kamel El Shenawy, Garden City"), then its
|
|
598
|
+
* locality, its admin districts, postcode and country; then by its two
|
|
599
|
+
* lines (`query`). Of each answer's streets, the one that names its
|
|
600
|
+
* district; else one in the district's own box as Azure has it (a little
|
|
601
|
+
* beyond it, as its point is a stretch's middle), asked for once; never a
|
|
602
|
+
* street of the name in another district (Azure's first "Kamel El Shenawy"
|
|
603
|
+
* is New Cairo's, 25 km from Garden City's), nor, for a street named by its
|
|
604
|
+
* number alone ("10 41 Street | Abu Tina"), one that does not say it is in
|
|
605
|
+
* its district where Azure knows none. Found nowhere so, its district
|
|
606
|
+
* (`precise: false`), else its town.
|
|
607
|
+
*
|
|
608
|
+
* An area by its own name (its neighbourhood, the place its line names, its
|
|
609
|
+
* locality; a governorate named for its town as the town: "Sohag", whose
|
|
610
|
+
* division's middle is 57 km from the city), never by its postcode (one of
|
|
611
|
+
* a town's many: Azure gives Tanta's 31111 for Tanta), or by its two lines
|
|
612
|
+
* where its neighbourhood is not its locality; a place with a name of its
|
|
613
|
+
* own by its two lines. Of places of one kind and name (Abu Dhabi's Al
|
|
614
|
+
* Bateen and Al Ain's), the one nearest where the search looked; found
|
|
615
|
+
* nowhere, the town it is in (`precise: false`).
|
|
616
|
+
*
|
|
617
|
+
* It is precise when the hit is the place itself (`isItself`), a street's
|
|
618
|
+
* in its own district: where Azure knows no box for the district, a street
|
|
619
|
+
* it marks `Ambiguous` (several of the words) is only near, as one of a
|
|
620
|
+
* number is unless it says it is in its district. Otherwise only near it.
|
|
621
|
+
* Never a place anywhere else: none is found then.
|
|
622
|
+
* A suggestion that names no region ("البرشاء 2", which Azure lists beside
|
|
623
|
+
* the same in Dubai) takes its city and region from the place found.
|
|
624
|
+
*/
|
|
625
|
+
async function azureLocate(geocode, place, locale) {
|
|
626
|
+
const { address: a, level, street, area: own = '', type, near } = place.azure;
|
|
627
|
+
const { place: locality, city, country } = azureWhere(a, locale);
|
|
628
|
+
const [first = [], second = []] = districtsOf(a);
|
|
629
|
+
const line = (0, address_line_1.addressLine)([place.label, place.detail]);
|
|
630
|
+
const around = {
|
|
631
|
+
adminDistrict: first[0],
|
|
632
|
+
adminDistrict2: second[0],
|
|
633
|
+
countryRegion: country || undefined,
|
|
634
|
+
};
|
|
635
|
+
const postalCode = text((0, answers_1.pathOf)(a, 'postalCode')) || undefined;
|
|
636
|
+
const nearby = near ? { coordinates: `${near.lng},${near.lat}` } : {};
|
|
637
|
+
// Places of one name in one district, ranked nearest where the search looked (the Al Bateen it found first); a street's stretches by Azure, as where the search looked may be a town away from the street.
|
|
638
|
+
const hits = async (params, ranked = level !== 'street') => {
|
|
639
|
+
const features = (0, answers_1.pathOf)(await geocode({ top: 5, ...params, ...(ranked ? nearby : {}) }), 'features');
|
|
640
|
+
return (Array.isArray(features) ? features : []).filter((hit) => Array.isArray((0, answers_1.pathOf)(hit, 'geometry', 'coordinates')) && fits(hit, a));
|
|
641
|
+
};
|
|
642
|
+
// An area of the town's size or less, nearest where the search looked of two of a name.
|
|
643
|
+
const areaOf = async (params) => {
|
|
644
|
+
const smaller = (await hits(params, true)).filter(isSmall);
|
|
645
|
+
return smaller[0] && nearestOf(smaller[0], smaller, near);
|
|
646
|
+
};
|
|
647
|
+
let hit;
|
|
648
|
+
let itself = false;
|
|
649
|
+
if (level === 'street') {
|
|
650
|
+
const district = districtOf(a, street, locale);
|
|
651
|
+
const numbered = isNumbered(street);
|
|
652
|
+
let ofDistrict;
|
|
653
|
+
const districtArea = () => (ofDistrict ??= areaOf({ locality: district, ...around }));
|
|
654
|
+
// A street Azure found only above what was asked (`UpHierarchy`) is the street itself only where it is of its name ("41 Street" for "10 41 Street"; never "Al Bateen Street" for "8").
|
|
655
|
+
const ownStreet = text((0, answers_1.pathOf)(a, 'streetName')) || street;
|
|
656
|
+
const isOurs = (one) => isStreet(one) && (!isAbove(one) || sameWords(text((0, answers_1.pathOf)(addressOf(one), 'streetName')), ownStreet));
|
|
657
|
+
// Of an answer's streets: with no district to keep to, the first (nearest where the search looked of two of a name); else the one that names its district, or one in its box.
|
|
658
|
+
const ofAnswer = async (found) => {
|
|
659
|
+
// The district itself, where an answer gives it ("El Mohandessin" for its two lines): not asked for again.
|
|
660
|
+
const theDistrict = district ? found.find((one) => !isStreet(one) && isSmall(one) && names(one, district)) : undefined;
|
|
661
|
+
if (theDistrict !== undefined)
|
|
662
|
+
ofDistrict ??= Promise.resolve(theDistrict);
|
|
663
|
+
const streets = found.filter(isOurs);
|
|
664
|
+
const exact = streets.filter((one) => isItself(one, level));
|
|
665
|
+
const order = [...exact, ...streets.filter((one) => !exact.includes(one))];
|
|
666
|
+
if (!order.length)
|
|
667
|
+
return {};
|
|
668
|
+
if (!district) {
|
|
669
|
+
const one = nearestOf(order[0], exact.length ? exact : streets, near);
|
|
670
|
+
return { home: { hit: one, itself: isItself(one, level) && !(numbered && isAmbiguous(one)) } };
|
|
671
|
+
}
|
|
672
|
+
const home = order.find((one) => names(one, district));
|
|
673
|
+
if (home)
|
|
674
|
+
return { home: { hit: home, itself: isItself(home, level) } };
|
|
675
|
+
const area = await districtArea();
|
|
676
|
+
// Another district's street of a number is another street; a road with a name, the same road where it is near enough. Where Azure knows no box for the district, a road with a name that says it is in no other district (`Ambiguous` only near).
|
|
677
|
+
const reach = numbered ? BEYOND_KM.numbered : BEYOND_KM.named;
|
|
678
|
+
const inside = order.find((one) => !(numbered && elsewhere(one, district)) && (area ? beyond(one, area) <= reach : !numbered && !elsewhere(one, district)));
|
|
679
|
+
return inside === undefined ? {} : { inside: { hit: inside, itself: isItself(inside, level) && (!!area || !isAmbiguous(inside)) && !(numbered && isAmbiguous(inside)) } };
|
|
680
|
+
};
|
|
681
|
+
// By its parts, its district for its locality and then its locality; a stretch that names its district before one only in its box; then by its two lines.
|
|
682
|
+
const byParts = [
|
|
683
|
+
district && !sameWords(district, locality) ? { addressLine: street, locality: district, postalCode, ...around } : undefined,
|
|
684
|
+
{ addressLine: street, locality: locality || undefined, postalCode, ...around },
|
|
685
|
+
];
|
|
686
|
+
let taken;
|
|
687
|
+
let inBox;
|
|
688
|
+
for (const ask of byParts) {
|
|
689
|
+
if (!ask || taken)
|
|
690
|
+
continue;
|
|
691
|
+
const { home, inside } = await ofAnswer(await hits(ask));
|
|
692
|
+
taken = home;
|
|
693
|
+
inBox ??= inside;
|
|
694
|
+
}
|
|
695
|
+
if (!taken && !inBox) {
|
|
696
|
+
const { home, inside } = await ofAnswer(await hits({ query: line }));
|
|
697
|
+
taken = home ?? inside;
|
|
698
|
+
}
|
|
699
|
+
taken ??= inBox;
|
|
700
|
+
if (taken)
|
|
701
|
+
({ hit, itself } = taken);
|
|
702
|
+
// Found nowhere in its district: the district, where Azure finds it, then its town.
|
|
703
|
+
if (!hit)
|
|
704
|
+
hit = (district && (await districtArea())) || (locality && (await areaOf({ locality, ...around })));
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
const neighbourhood = text((0, answers_1.pathOf)(a, 'neighborhood'));
|
|
708
|
+
const byName = level === 'named' || (neighbourhood && !sameWords(neighbourhood, locality));
|
|
709
|
+
const name = own || locality;
|
|
710
|
+
let found = await hits(byName ? { query: line } : name ? { locality: name, ...around } : { ...around });
|
|
711
|
+
if (!found.length && !byName)
|
|
712
|
+
found = await hits(name ? { query: line } : { ...around });
|
|
713
|
+
// A governorate named for its town is meant as the town.
|
|
714
|
+
const town = (one) => Number(type === 'AdminDivision2' && (0, answers_1.pathOf)(one, 'properties', 'type') === 'PopulatedPlace');
|
|
715
|
+
const exact = found.filter((one) => isItself(one, level));
|
|
716
|
+
const order = [...exact, ...found.filter((one) => !exact.includes(one))].sort((x, y) => town(y) - town(x));
|
|
717
|
+
const likeliest = level === 'area' ? (order.find(isSmall) ?? order[0]) : order[0];
|
|
718
|
+
hit = likeliest && nearestOf(likeliest, order, near);
|
|
719
|
+
itself = !!hit && isItself(hit, level);
|
|
720
|
+
if (!hit)
|
|
721
|
+
hit = level === 'area' ? city && !sameWords(city, name) && (await areaOf({ locality: city, ...around })) : locality && (await areaOf({ locality, ...around }));
|
|
722
|
+
}
|
|
723
|
+
if (!hit)
|
|
724
|
+
throw new Error(`Azure Maps found "${(0, address_line_1.addressLine)([place.label, place.detail], locale)}" nowhere in its own district and town`);
|
|
725
|
+
const at = pointOf(hit);
|
|
726
|
+
// A suggestion that names no region: its city and region, and its line, as the place found names them.
|
|
727
|
+
const named = districtsOf(a).length ? a : { ...a, adminDistricts: (0, answers_1.pathOf)(addressOf(hit), 'adminDistricts') };
|
|
728
|
+
const where = azureWhere(named, locale);
|
|
729
|
+
const parts = place.parts && { ...place.parts, ...(where.region ? { region: where.region } : {}), ...(place.parts.city || where.city ? { city: cityAt(named, at, locale) } : {}) };
|
|
730
|
+
const detail = named === a ? place.detail : azureLine(named, locale, '', [place.label], text((0, answers_1.pathOf)(a, 'addressLine')));
|
|
731
|
+
return { ...at, address: (0, address_line_1.addressLine)([place.label, detail], locale), ...(parts ? { parts } : {}), ...(itself ? {} : { precise: false }) };
|
|
732
|
+
}
|
|
733
|
+
// ---- Google ------------------------------------------------------------------------
|
|
734
|
+
/** A plus code ("2XFJ+VJ6"): Google's name for a spot with no address of its own, never the place a person picked. */
|
|
735
|
+
const PLUS_CODE = /^[23456789CFGHJMPQRVWX]{4,8}\+[23456789CFGHJMPQRVWX]{0,3}(\s|$)/i;
|
|
736
|
+
/** What Google may put before a house's number (": 15 شارع التحرير"). */
|
|
737
|
+
const STRAY = /^[\s:;,،.\-–]+/u;
|
|
738
|
+
/** One of Google's `addressComponents`, by its type. */
|
|
739
|
+
const componentOf = (components, type) => (Array.isArray(components) ? components : []).find((c) => (0, answers_1.pathOf)(c, 'types')?.includes(type));
|
|
740
|
+
/** Every country's name in a language and in English, as the browser names them: by the page's language, once. */
|
|
741
|
+
const COUNTRY_NAMES = new Map();
|
|
742
|
+
function isCountry(name, locale) {
|
|
743
|
+
let names = COUNTRY_NAMES.get(locale);
|
|
744
|
+
if (!names) {
|
|
745
|
+
names = new Set();
|
|
746
|
+
for (const language of new Set([locale, 'en'])) {
|
|
747
|
+
const named = regionNames(language);
|
|
748
|
+
for (let a = 65; a <= 90; a++)
|
|
749
|
+
for (let b = 65; b <= 90; b++) {
|
|
750
|
+
try {
|
|
751
|
+
const one = named?.of(String.fromCharCode(a, b));
|
|
752
|
+
if (one)
|
|
753
|
+
names.add(one);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
// A code the browser names nothing for.
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
COUNTRY_NAMES.set(locale, names);
|
|
761
|
+
}
|
|
762
|
+
return names.has(name);
|
|
763
|
+
}
|
|
764
|
+
/** A dash between two pieces of a line, as Google writes the UAE's ("Sheikh Zayed Rd - Dubai - United Arab Emirates"). */
|
|
765
|
+
const DASH = /\s+[-–—]\s+/u;
|
|
766
|
+
/**
|
|
767
|
+
* A line Google writes with dashes between its pieces, the UAE's, with
|
|
768
|
+
* commas there: each piece then said once, and joined as the page joins an
|
|
769
|
+
* address's ("Burj Khalifa" not again after the landmark's own name). Only
|
|
770
|
+
* a line whose last piece, after a dash, is its country (`country`, as its
|
|
771
|
+
* parts name it; or any country's name, for a suggestion's line): a dash in
|
|
772
|
+
* a name ("26 July Axis - El Shiakh Zaied", "Mobil Gas Station - Zamalek")
|
|
773
|
+
* stays.
|
|
774
|
+
*/
|
|
775
|
+
function undashed(line, locale, country = '') {
|
|
776
|
+
const pieces = line.split(DASH);
|
|
777
|
+
const last = pieces[pieces.length - 1];
|
|
778
|
+
return pieces.length > 1 && (country ? last === country : isCountry(last, locale)) ? pieces.join(', ') : line;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Google's `addressComponents`, each by its type: the country by its short
|
|
782
|
+
* code. Its number where Google's own line (`formatted`) puts it: before the
|
|
783
|
+
* street in Egypt and Saudi Arabia ("7524 King Fahd Rd"), after it in Jordan
|
|
784
|
+
* and Germany ("Rainbow St. 15"). Its city its locality; Egypt's streets have
|
|
785
|
+
* none, and their district stands for it ("Qasr El Nil", "Maadi"), by its
|
|
786
|
+
* name alone in Arabic too ("الدقي", not "قسم الدقي", its police district's).
|
|
787
|
+
*/
|
|
788
|
+
const googleParts = (components, formatted) => {
|
|
789
|
+
const of = (type, short) => text((0, answers_1.pathOf)(componentOf(components, type), short ? 'shortText' : 'longText'));
|
|
790
|
+
// A number has a digit: Google's "شارع" ("street") before a street on an English page is none.
|
|
791
|
+
const said = of('street_number').replace(STRAY, '');
|
|
792
|
+
const number = /\p{Nd}/u.test(said) ? said : '';
|
|
793
|
+
const line = text(formatted);
|
|
794
|
+
const numberAt = number ? line.search(new RegExp(`(^|\\P{N})${number.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\p{N})`, 'u')) : -1;
|
|
795
|
+
const streetAt = [of('route'), of('route', true)].map((name) => (name ? line.indexOf(name) : -1)).filter((at) => at >= 0);
|
|
796
|
+
const postcode = of('postal_code');
|
|
797
|
+
// A Saudi postcode's additional number, a ZIP code's four: "13521-4273".
|
|
798
|
+
const suffix = of('postal_code_suffix');
|
|
799
|
+
return partsOf({
|
|
800
|
+
street: of('route'),
|
|
801
|
+
number,
|
|
802
|
+
numberFirst: numberAt >= 0 && (!streetAt.length || numberAt < Math.min(...streetAt)),
|
|
803
|
+
city: (of('locality') || of('postal_town') || of('administrative_area_level_2') || of('sublocality_level_1') || of('administrative_area_level_3')).replace(/^قسم\s+/u, ''),
|
|
804
|
+
region: of('administrative_area_level_1'),
|
|
805
|
+
postcode: postcode && suffix ? `${postcode}-${suffix}` : postcode,
|
|
806
|
+
country: of('country', true),
|
|
807
|
+
});
|
|
808
|
+
};
|
|
809
|
+
exports.googleParts = googleParts;
|
|
810
|
+
/**
|
|
811
|
+
* Google's suggestions: each one's name, and where it is; for an address
|
|
812
|
+
* field, the addresses first (their types say "geocode"), the shops and
|
|
813
|
+
* buildings after them. Each is located when picked, through `details`
|
|
814
|
+
* (Place Details), which ends its session (`end`), as picking it does.
|
|
815
|
+
*/
|
|
816
|
+
function googlePlaces(answer, { locale, addresses, parts }, details, end) {
|
|
817
|
+
const suggestions = (0, answers_1.pathOf)(answer, 'suggestions');
|
|
818
|
+
if (!Array.isArray(suggestions))
|
|
819
|
+
return [];
|
|
820
|
+
const found = suggestions.flatMap((suggestion) => {
|
|
821
|
+
const prediction = (0, answers_1.pathOf)(suggestion, 'placePrediction');
|
|
822
|
+
const placeId = (0, answers_1.pathOf)(prediction, 'placeId');
|
|
823
|
+
const label = (text((0, answers_1.pathOf)(prediction, 'structuredFormat', 'mainText', 'text')) || text((0, answers_1.pathOf)(prediction, 'text', 'text'))).replace(STRAY, '');
|
|
824
|
+
if (typeof placeId !== 'string' || !label)
|
|
825
|
+
return [];
|
|
826
|
+
// Where it is, without its name again: Google's line under a road may start with the road ("Sheikh Zayed Road - Dubai - …").
|
|
827
|
+
const detail = (0, address_line_1.addressLine)([undashed(text((0, answers_1.pathOf)(prediction, 'structuredFormat', 'secondaryText', 'text')), locale)], locale, [label]);
|
|
828
|
+
const types = (0, answers_1.pathOf)(prediction, 'types');
|
|
829
|
+
// A building, a shop, a mall: a place with a name of its own, which its address may not say.
|
|
830
|
+
const named = Array.isArray(types) && !types.includes('geocode');
|
|
831
|
+
const place = {
|
|
832
|
+
label,
|
|
833
|
+
...(detail ? { detail } : {}),
|
|
834
|
+
picked: end,
|
|
835
|
+
async locate() {
|
|
836
|
+
try {
|
|
837
|
+
const at = await details(placeId);
|
|
838
|
+
const lat = (0, answers_1.pathOf)(at, 'location', 'latitude');
|
|
839
|
+
const lng = (0, answers_1.pathOf)(at, 'location', 'longitude');
|
|
840
|
+
if (typeof lat !== 'number' || typeof lng !== 'number')
|
|
841
|
+
throw new Error('Google gave no place');
|
|
842
|
+
// Its country set apart where Google writes nothing but a space before it ("King Fahd Rd, Riyadh Saudi Arabia"), as its parts name it; the UAE's dashes read as commas.
|
|
843
|
+
const country = text((0, answers_1.pathOf)(componentOf((0, answers_1.pathOf)(at, 'addressComponents'), 'country'), 'longText'));
|
|
844
|
+
const pieces = undashed(text((0, answers_1.pathOf)(at, 'formattedAddress')), locale, country)
|
|
845
|
+
.split(/[,،]/)
|
|
846
|
+
.map((piece) => piece.trim())
|
|
847
|
+
.flatMap((piece) => {
|
|
848
|
+
const before = country && piece !== country && piece.endsWith(` ${country}`) ? piece.slice(0, -country.length).trim() : '';
|
|
849
|
+
return before && !/[-–—]$/u.test(before) ? [before, country] : [piece];
|
|
850
|
+
});
|
|
851
|
+
// Without a plus code before it, nor what joined the code to it ("67GM+6V6 - Sheikh Zayed Rd").
|
|
852
|
+
pieces[0] = pieces[0].replace(STRAY, '').replace(PLUS_CODE, '').replace(STRAY, '');
|
|
853
|
+
const address = (0, address_line_1.addressLine)(named ? [label, ...pieces] : pieces, locale);
|
|
854
|
+
const own = parts ? (0, exports.googleParts)((0, answers_1.pathOf)(at, 'addressComponents'), (0, answers_1.pathOf)(at, 'formattedAddress')) : undefined;
|
|
855
|
+
return { lat, lng, ...(address ? { address } : {}), ...(own ? { parts: own } : {}) };
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
end();
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
return [{ place, named }];
|
|
863
|
+
});
|
|
864
|
+
return (addresses ? [...found.filter((p) => !p.named), ...found.filter((p) => p.named)] : found).map(({ place }) => place);
|
|
865
|
+
}
|
|
47
866
|
//# sourceMappingURL=address-parts.js.map
|