diff --git a/README.md b/README.md
index 9cc0e5836..3fcadf105 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ The control comes with a number of default providers:
- [LocationIQ]
- [OpenCage]
- [OpenStreetMap]
+- [PdokNl]
- [Pelias]
- [Mapbox](https://docs.mapbox.com/help/tutorials/local-search-geocoding-api/)
- [GeoApiFR](https://geo.api.gouv.fr/adresse)
@@ -352,6 +353,7 @@ refresh when you change source files.
[locationiq]: https://leaflet-geosearch.meijer.works/providers/locationiq
[opencage]: https://leaflet-geosearch.meijer.works/providers/opencage
[openstreetmap]: https://leaflet-geosearch.meijer.works/providers/openstreetmap
+[pdoknl]: https://leaflet-geosearch.meijer.works/providers/pdoknl
[pelias]: https://leaflet-geosearch.meijer.works/providers/pelias
## Contributors ✨
diff --git a/docs/lib/providers.ts b/docs/lib/providers.ts
index 61285d9e3..36c4ec44f 100644
--- a/docs/lib/providers.ts
+++ b/docs/lib/providers.ts
@@ -11,6 +11,7 @@ import {
LocationIQProvider,
OpenCageProvider,
OpenStreetMapProvider,
+ PdokNlProvider,
PeliasProvider,
} from 'leaflet-geosearch';
@@ -54,5 +55,7 @@ export default {
OpenStreetMap: new OpenStreetMapProvider(),
+ PdokNl: new PdokNlProvider(),
+
Pelias: new PeliasProvider(),
};
diff --git a/docs/providers/pdoknl.mdx b/docs/providers/pdoknl.mdx
new file mode 100644
index 000000000..3eb92e63e
--- /dev/null
+++ b/docs/providers/pdoknl.mdx
@@ -0,0 +1,51 @@
+---
+name: PdokNl
+menu: Providers
+route: /providers/pdoknl
+---
+
+import Playground from '../components/Playground';
+import Map from '../components/Map';
+
+# PdokNl Provider
+
+For more options and configurations, see the [PDOK Locatieserver][1].
+
+
+
+
+
+```js
+import { PdokNlProvider } from 'leaflet-geosearch';
+
+const provider = new PdokNlProvider();
+
+// add to leaflet
+import { GeoSearchControl } from 'leaflet-geosearch';
+
+map.addControl(
+ new GeoSearchControl({
+ provider,
+ style: 'bar',
+ }),
+);
+```
+
+## Optional parameters
+
+PDOK Locatieserver supports a number of [optional parameters][2]. As the api requires those parameters to be added to the url, they can be added to the `params` key of the provider.
+
+All options defined next to the `params` key, would have been added to the request body.
+
+```js
+const provider = new PdokNlProvider({
+ params: {
+ fq: 'gemeentenaam:Leiden', // limit search results to municipality
+ lat: 0, // Latitude in degree
+ lon: 0, // Longitude in degree
+ },
+});
+```
+
+[1]: https://www.pdok.nl/pdok-locatieserver
+[2]: https://api.pdok.nl/bzk/locatieserver/search/v3_1/ui/#/Locatieserver/suggest
diff --git a/src/index.ts b/src/index.ts
index 20e177bfc..23832934f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -17,5 +17,6 @@ export { default as MapBoxProvider } from './providers/mapBoxProvider';
export { default as OpenCageProvider } from './providers/openCageProvider';
export { default as OpenStreetMapProvider } from './providers/openStreetMapProvider';
export { default as PeliasProvider } from './providers/peliasProvider';
+export { default as PdokNlProvider } from './providers/pdokNlProvider';
export { default as JsonProvider } from './providers/provider';
diff --git a/src/providers/__tests__/pdokNlProvider.spec.js b/src/providers/__tests__/pdokNlProvider.spec.js
new file mode 100644
index 000000000..9f6981bed
--- /dev/null
+++ b/src/providers/__tests__/pdokNlProvider.spec.js
@@ -0,0 +1,25 @@
+import Provider from '../pdokNlProvider';
+import fixtures from './pdokNlResponse.json';
+
+describe('PdokNlProvider', () => {
+ beforeAll(() => {
+ fetch.mockResponse(async () => ({ body: JSON.stringify(fixtures) }));
+ });
+
+ test('Can fetch results', async () => {
+ const provider = new Provider();
+
+ const results = await provider.search({ query: '3e Binnenvestgracht' });
+ const result = results[0];
+
+ const position = fixtures.response.docs[0].centroide_ll
+ .replace(/POINT\(|\)/g, '')
+ .trim()
+ .split(' ')
+ .map(Number);
+
+ expect(result.label).toBeTruthy();
+ expect(result.x).toEqual(position[0]);
+ expect(result.y).toEqual(position[1]);
+ });
+});
diff --git a/src/providers/__tests__/pdokNlResponse.json b/src/providers/__tests__/pdokNlResponse.json
new file mode 100644
index 000000000..793806267
--- /dev/null
+++ b/src/providers/__tests__/pdokNlResponse.json
@@ -0,0 +1,64 @@
+{
+ "response": {
+ "numFound": 49,
+ "start": 0,
+ "maxScore": 10.934887,
+ "numFoundExact": true,
+ "docs": [
+ {
+ "weergavenaam": "3e Binnenvestgracht, 2312NR Leiden",
+ "id": "pcd-f14950155195f418398b6eda76d66adb",
+ "centroide_ll": "POINT(4.49093267 52.16412132)"
+ },
+ {
+ "weergavenaam": "3e Binnenvestgracht 23L-1, 2312NR Leiden",
+ "id": "adr-37dba6d7e92c0ccca8fc738005057bd9",
+ "centroide_ll": "POINT(4.49157312 52.16420333)"
+ },
+ {
+ "weergavenaam": "3e Binnenvestgracht 23L-2, 2312NR Leiden",
+ "id": "adr-fd7be59d3e4ea77dafba1e8fd69017e6",
+ "centroide_ll": "POINT(4.49157 52.16425474)"
+ },
+ {
+ "weergavenaam": "3e Binnenvestgracht 23T-1, 2312NR Leiden",
+ "id": "adr-763525b75ad60b4e13fdf3d00b195912",
+ "centroide_ll": "POINT(4.49202289 52.164203)"
+ },
+ {
+ "weergavenaam": "3e Binnenvestgracht 23A, 2312NR Leiden",
+ "id": "adr-6d98d9468e29425d0ad81dcfdbe00f7d",
+ "centroide_ll": "POINT(4.49098397 52.16416908)"
+ }
+ ]
+ },
+ "highlighting": {
+ "pcd-f14950155195f418398b6eda76d66adb": {
+ "suggest": ["3e Binnenvestgracht, 2312 NR Leiden"]
+ },
+ "adr-37dba6d7e92c0ccca8fc738005057bd9": {
+ "suggest": [
+ "3e Binnenvestgracht 23L-1, 2312NR Leiden"
+ ]
+ },
+ "adr-fd7be59d3e4ea77dafba1e8fd69017e6": {
+ "suggest": [
+ "3e Binnenvestgracht 23L-2, 2312NR Leiden"
+ ]
+ },
+ "adr-763525b75ad60b4e13fdf3d00b195912": {
+ "suggest": [
+ "3e Binnenvestgracht 23T-1, 2312NR Leiden"
+ ]
+ },
+ "adr-6d98d9468e29425d0ad81dcfdbe00f7d": {
+ "suggest": [
+ "3e Binnenvestgracht 23A, 2312NR Leiden"
+ ]
+ }
+ },
+ "spellcheck": {
+ "suggestions": [],
+ "collations": []
+ }
+}
diff --git a/src/providers/index.ts b/src/providers/index.ts
index 470f52f68..524ef1953 100644
--- a/src/providers/index.ts
+++ b/src/providers/index.ts
@@ -12,6 +12,7 @@ export { default as LocationIQProvider } from './locationIQProvider';
export { default as MapBoxProvider } from './mapBoxProvider';
export { default as OpenCageProvider } from './openCageProvider';
export { default as OpenStreetMapProvider } from './openStreetMapProvider';
+export { default as PdokNlProvider } from './pdokNlProvider';
export { default as PeliasProvider } from './peliasProvider';
export { default as Provider } from './provider';
diff --git a/src/providers/pdokNlProvider.ts b/src/providers/pdokNlProvider.ts
new file mode 100644
index 000000000..9dde1651d
--- /dev/null
+++ b/src/providers/pdokNlProvider.ts
@@ -0,0 +1,143 @@
+import AbstractProvider, {
+ EndpointArgument,
+ ParseArgument,
+ ProviderOptions,
+ SearchResult,
+ RequestType,
+ SearchArgument,
+} from './provider';
+
+interface Doc {
+ weergavenaam: string;
+ id: string;
+ centroide_ll: string;
+}
+
+export interface RequestResult {
+ response: {
+ numFound: number;
+ start: number;
+ maxScore: number;
+ numFoundExact: boolean;
+ docs: Doc[];
+ };
+ highlighting: {
+ [key: string]: {
+ suggest: string[];
+ };
+ };
+ spellcheck: {
+ suggestions: [
+ string,
+ {
+ numFound: number;
+ startOffset: number;
+ endOffset: number;
+ suggestion: string[];
+ },
+ ];
+ collations: [
+ 'collation',
+ {
+ collationQuery: string;
+ hits: number;
+ misspellingsAndCorrections: string[];
+ },
+ ];
+ };
+}
+
+export interface RawResult extends Doc {
+ highlight: string;
+}
+
+export type PdokNlProviderOptions = ProviderOptions;
+
+export default class PdokNlProvider extends AbstractProvider<
+ RequestResult,
+ RawResult
+> {
+ searchUrl: string;
+ reverseUrl: string;
+
+ constructor(options: PdokNlProviderOptions = {}) {
+ super({
+ ...options,
+ params: {
+ bq: 'type:gemeente^0.5 type:woonplaats^0.5 type:weg^1.0 type:postcode^1.5 type:adres^2.0',
+ fl: 'id,weergavenaam,centroide_ll',
+ rows: 5,
+ ...options.params,
+ },
+ });
+
+ const base = 'https://api.pdok.nl/bzk/locatieserver/search/v3_1';
+ this.searchUrl = `${base}/suggest`;
+ this.reverseUrl = `${base}/reverse`;
+ }
+
+ endpoint({ query, type }: EndpointArgument) {
+ const params = typeof query === 'string' ? { q: query } : query;
+
+ switch (type) {
+ case RequestType.REVERSE:
+ return this.getUrl(this.reverseUrl, params);
+
+ default:
+ return this.getUrl(this.searchUrl, params);
+ }
+ }
+
+ parse({ data }: ParseArgument) {
+ return data.response.docs.map((doc) => {
+ const position = doc.centroide_ll
+ .replace(/POINT\(|\)/g, '')
+ .trim()
+ .split(' ')
+ .map(Number);
+
+ let highlight = doc.weergavenaam;
+ if (data.highlighting[doc.id].suggest) {
+ highlight = data.highlighting[doc.id].suggest[0];
+ }
+
+ return {
+ x: position[0],
+ y: position[1],
+ label: doc.weergavenaam,
+ bounds: null,
+ raw: {
+ ...doc,
+ highlight,
+ },
+ };
+ });
+ }
+
+ async search(options: SearchArgument): Promise[]> {
+ const url = this.endpoint({
+ query: options.query,
+ });
+
+ const json = await fetch(url)
+ .then((response) => response.json())
+ .then((json: RequestResult) => {
+ // If there are no results found but there are spellcheck corrections, we search again using the corrected query.
+ if (json.response.numFound < 1) {
+ if (json.spellcheck.collations.length >= 2) {
+ const url = this.endpoint({
+ query: json.spellcheck.collations[1].collationQuery,
+ });
+
+ return fetch(url).then(
+ (response): Promise => response.json(),
+ );
+ }
+ }
+
+ return json;
+ });
+
+ return this.parse({ data: json });
+ }
+}