Skip to content

Commit 5bb5635

Browse files
committed
Nominatim: limit requests to one per second
The Nominatim usage policy allows "an absolute maximum of 1 request per second", but nothing throttled submits: holding Enter or clicking the search button repeatedly sent one request per keypress. Queue geocode() and reverse() behind a per-instance promise chain that spaces the start of consecutive requests at least a second apart. Requests are delayed rather than dropped, and the chain resolves to a timestamp so a failed request cannot stall the ones behind it. https://operations.osmfoundation.org/policies/nominatim/
1 parent 68bfbf6 commit 5bb5635

1 file changed

Lines changed: 31 additions & 0 deletions

File tree

src/geocoders/nominatim.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ export interface NominatimOptions extends GeocoderOptions {
5656
htmlTemplate: (r: NominatimResult) => string;
5757
}
5858

59+
/**
60+
* The [Nominatim usage policy](https://operations.osmfoundation.org/policies/nominatim/) permits
61+
* "an absolute maximum of 1 request per second".
62+
* @internal
63+
*/
64+
const MIN_REQUEST_INTERVAL = 1000;
65+
5966
/**
6067
* Implementation of the [Nominatim](https://wiki.openstreetmap.org/wiki/Nominatim) geocoder.
6168
*
@@ -90,17 +97,40 @@ export class Nominatim implements IGeocoder {
9097
}
9198
};
9299

100+
/**
101+
* Start time of the most recent request, as a promise chain, used to space requests apart.
102+
*/
103+
private _lastRequestStart: Promise<number> = Promise.resolve(0);
104+
93105
constructor(options?: Partial<NominatimOptions>) {
94106
L.Util.setOptions(this, options || {});
95107
}
96108

109+
/**
110+
* Resolves once the next request may be sent, that is at least
111+
* {@link MIN_REQUEST_INTERVAL} after the previous one was started. Callers are queued in
112+
* the order they call this, and are delayed rather than dropped.
113+
*/
114+
private _rateLimit(): Promise<unknown> {
115+
const scheduled = this._lastRequestStart.then(async last => {
116+
const wait = MIN_REQUEST_INTERVAL - (Date.now() - last);
117+
if (wait > 0) {
118+
await new Promise(resolve => setTimeout(resolve, wait));
119+
}
120+
return Date.now();
121+
});
122+
this._lastRequestStart = scheduled;
123+
return scheduled;
124+
}
125+
97126
async geocode(query: string) {
98127
const params = geocodingParams(this.options, {
99128
q: query,
100129
limit: 5,
101130
format: 'json',
102131
addressdetails: 1
103132
});
133+
await this._rateLimit();
104134
const data = await getJSON<NominatimResult[]>(this.options.serviceUrl + 'search', params);
105135
return data.map((item): GeocodingResult => {
106136
const bbox = item.boundingbox;
@@ -138,6 +168,7 @@ export class Nominatim implements IGeocoder {
138168
addressdetails: 1,
139169
format: 'json'
140170
});
171+
await this._rateLimit();
141172
const data = await getJSON<NominatimResult>(this.options.serviceUrl + 'reverse', params);
142173
if (!data?.lat || !data?.lon) {
143174
return [];

0 commit comments

Comments
 (0)