Skip to content

Commit 89ff7ae

Browse files
committed
feat: azure maps geocoder
1 parent 935aac9 commit 89ff7ae

File tree

7 files changed

+657
-1
lines changed

7 files changed

+657
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ Features:
2727
| Provider | forward | reverse | ip | Notes |
2828
| ----------------------------------------------------------------------------------------------- | :-----: | :-----: | :-: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2929
| [ArcGisGeocoder](https://developers.arcgis.com/documentation/mapping-apis-and-services/search/) |||| |
30-
| [BingMapsGeocoder](https://docs.microsoft.com/en-us/bingmaps/rest-services/locations) |||| results are in English only |
30+
| [AzureMapsGeocoder](https://learn.microsoft.com/en-us/rest/api/maps/search?view=rest-maps-2025-01-01) |||| |
31+
| ~~[BingMapsGeocoder](https://docs.microsoft.com/en-us/bingmaps/rest-services/locations)~~ DEPRECATED |||| results are in English only |
3132
| [GoogleGeocoder](https://developers.google.com/maps/documentation/geocoding/overview) |||| |
3233
| [GeocodioGeocoder](https://www.geocod.io/docs/) |||| results are in English only; Country must be part of query, otherwise fallback to US; [Only US and major cities in CA supported](https://www.geocod.io/coverage/) |
3334
| [HereGeocoder](https://developer.here.com/) |||| |

env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ OSM_TEST=true
1212
# Add your API keys here to run the examples
1313

1414
ARCGIS_APIKEY=
15+
AZUREMAPS_APIKEY=
1516
BINGMAPS_APIKEY=
1617
GEOCODE_APIKEY=
1718
GEOCODIO_APIKEY=

examples/azuremaps.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import dotenv from 'dotenv'
2+
import { argv } from './argv.js'
3+
import { fetchAdapter, AzureMapsGeocoder } from '../src/index.js'
4+
5+
dotenv.config()
6+
7+
const { AZUREMAPS_APIKEY: apiKey, FORWARD, REVERSE, LANGUAGE } = process.env
8+
const { forward, reverse, ...other } = argv({
9+
forward: FORWARD,
10+
reverse: REVERSE
11+
})
12+
13+
const adapter = fetchAdapter()
14+
const geocoder = new AzureMapsGeocoder(adapter, {
15+
apiKey,
16+
language: LANGUAGE,
17+
...other
18+
})
19+
20+
const promise = reverse ? geocoder.reverse(reverse) : geocoder.forward(forward)
21+
promise.then((res) => console.dir(res, { depth: null })).catch(console.error)

src/geocoder/azuremaps.js

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { AbstractGeocoder } from './abstract.js'
2+
import { HttpError } from '../utils/index.js'
3+
4+
/** @typedef {import('../adapter.js').fetchAdapterFn} fetchAdapterFn */
5+
6+
const hasResult = (result) => {
7+
const { features } = result || {}
8+
return Array.isArray(features)
9+
}
10+
11+
const CONFIDENCE_MAP = {
12+
High: 1,
13+
Medium: 0.6,
14+
Low: 0.3
15+
}
16+
17+
const ACCEPT_LANGUAGE = 'accept-language'
18+
19+
/**
20+
* see https://learn.microsoft.com/en-us/rest/api/maps/search/get-geocoding?view=rest-maps-2025-01-01&tabs=HTTP
21+
* @typedef {object} AzureMapsForwardQuery
22+
* @property {string} address (equals query)
23+
* @property {string} [addressLine]
24+
* @property {string} [adminDistrict]
25+
* @property {string} [adminDistrict2]
26+
* @property {string} [adminDistrict3]
27+
* @property {number[]} [bbox]
28+
* @property {number[]} [coordinates]
29+
* @property {string} [countryRegion]
30+
* @property {string} [locality]
31+
* @property {string} [postalCode]
32+
* @property {string} [view]
33+
* @property {number} [limit] (equals top)
34+
* @property {string} [language]
35+
*/
36+
37+
/**
38+
* see
39+
* @typedef {object} BingMapsReverseQuery
40+
* @property {number} lat latitude
41+
* @property {number} lng longitude
42+
* @property {number} [limit]
43+
* @property {string} [language]
44+
*/
45+
46+
export class AzureMapsGeocoder extends AbstractGeocoder {
47+
/**
48+
* available options
49+
* @see https://learn.microsoft.com/en-us/rest/api/maps/search?view=rest-maps-2025-01-01
50+
* @param {fetchAdapterFn} adapter
51+
* @param {object} options
52+
* @param {string} [options.apiKey] - subscription key
53+
* @param {string} [options.authorization] - authorization header if using OAuth2
54+
* @param {number} [options.limit]
55+
* @param {string} [options.language]
56+
*/
57+
constructor(adapter, options = { apiKey: '' }) {
58+
// @ts-ignore
59+
super(adapter, options)
60+
61+
if (!options.apiKey && !options.authorization) {
62+
throw new Error(
63+
'You must specify some authorization or apiKey to use AzureMapsGeocoder'
64+
)
65+
}
66+
67+
const { limit: top, language, apiKey } = options
68+
69+
this.headers = {
70+
[ACCEPT_LANGUAGE]: 'en'
71+
}
72+
setLanguage(this.headers, language)
73+
74+
this.params = {
75+
'api-version': '2025-01-01',
76+
'subscription-key': apiKey,
77+
top
78+
}
79+
}
80+
81+
get endpoint() {
82+
return 'https://atlas.microsoft.com/geocode'
83+
}
84+
85+
get revEndpoint() {
86+
return 'https://atlas.microsoft.com/reverseGeocode'
87+
}
88+
89+
/**
90+
* @param {AzureMapsForwardQuery|string} query
91+
* @returns {Promise<object>}
92+
*/
93+
async _forward(query = '') {
94+
let params = {}
95+
let headers = { ...this.headers }
96+
97+
if (typeof query === 'string') {
98+
params = { ...this.params, query }
99+
} else {
100+
const { address, limit, language, ...other } = query
101+
setLanguage(headers, language)
102+
103+
const top = limit || this.params.top
104+
if (address) {
105+
const { bbox, coordinates, view } = other
106+
params = { ...params, top, query: address, bbox, coordinates, view }
107+
} else {
108+
params = { ...params, top, ...other }
109+
}
110+
}
111+
112+
const url = this.createUrl(this.endpoint, params)
113+
114+
const res = await this.adapter(url, { headers })
115+
if (res.status !== 200) {
116+
throw HttpError(res)
117+
}
118+
const result = await res.json()
119+
// console.dir(result, { depth: null })
120+
if (!hasResult(result)) {
121+
return this.wrapRaw([], result)
122+
}
123+
const results = result.features.map(this._formatResult)
124+
return this.wrapRaw(results, result)
125+
}
126+
127+
/**
128+
* @todo
129+
* @param {BingMapsReverseQuery} query
130+
* @returns {Promise<object>}
131+
*/
132+
async _reverse(query) {
133+
const { lat, lng, language, limit: top, ...other } = query
134+
const params = {
135+
...this.params,
136+
...other,
137+
coordinates: `${lng},${lat}`,
138+
top
139+
}
140+
let headers = { ...this.headers }
141+
setLanguage(headers, language)
142+
143+
const url = this.createUrl(this.revEndpoint, params)
144+
145+
const res = await this.adapter(url, { headers })
146+
if (res.status !== 200) {
147+
throw HttpError(res)
148+
}
149+
const result = await res.json()
150+
// console.dir(result, { depth: null })
151+
if (!hasResult(result)) {
152+
return this.wrapRaw([], result)
153+
}
154+
const results = result.features.map(this._formatResult)
155+
return this.wrapRaw(results, result)
156+
}
157+
158+
/**
159+
* @see https://learn.microsoft.com/en-us/rest/api/maps/search/get-geocoding?view=rest-maps-2025-01-01&tabs=HTTP#featuresitem
160+
*/
161+
_formatResult(result) {
162+
const { bbox, geometry, properties = {} } = result || {}
163+
const { address, confidence } = properties
164+
165+
const extra = {
166+
confidence: CONFIDENCE_MAP[confidence] || 0,
167+
bbox
168+
}
169+
170+
const formatted = {
171+
formattedAddress: address.formattedAddress,
172+
latitude: geometry.coordinates[1],
173+
longitude: geometry.coordinates[0],
174+
country: address.countryRegion?.name,
175+
countryCode: address.countryRegion?.ISO,
176+
state: address.adminDistrict?.[0],
177+
region: address.adminDistrict?.[1],
178+
city: address.locality,
179+
zipcode: address.postalCode,
180+
streetName: address.addressLine,
181+
extra
182+
}
183+
184+
return formatted
185+
}
186+
}
187+
188+
const setLanguage = (headers, language) => {
189+
if (!language) return
190+
headers[ACCEPT_LANGUAGE] = language
191+
}

src/geocoder/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @ts-nocheck
22
export * from './abstract.js'
33
export * from './arcgis.js'
4+
export * from './azuremaps.js'
45
export * from './bingmaps.js'
56
export * from './geocodio.js'
67
export * from './google.js'

0 commit comments

Comments
 (0)