diff --git a/mocks/fixtures/bag.json b/mocks/fixtures/bag.json deleted file mode 100644 index 6bdd46bb56..0000000000 --- a/mocks/fixtures/bag.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "_links": { - "self": { - "href": "https://api.data.amsterdam.nl/atlas/search/adres/?q=Burgemeester%20R%C3%B6ellstr%201&page=1" - }, - "next": { - "href": null - }, - "prev": { - "href": null - } - }, - "count_hits": 38, - "count": 38, - "results": [ - { - "_links": { - "self": { - "href": "https://api.data.amsterdam.nl/bag/verblijfsobject/0363010000599756/" - } - }, - "type": "verblijfsobject", - "dataset": "nummeraanduiding", - "adres": "Burgemeester Röellstraat 1", - "postcode": "1064BH", - "straatnaam": "Burgemeester Röellstraat", - "huisnummer": 1, - "toevoeging": "1", - "bag_huisletter": "", - "bag_toevoeging": "", - "woonplaats": "Amsterdam", - "hoofdadres": true, - "status": [ - { - "code": "16", - "omschrijving": "Naamgeving uitgegeven" - } - ], - "landelijk_id": "0363200000062087", - "vbo_status": [ - { - "code": "21", - "omschrijving": "Verblijfsobject in gebruik" - } - ], - "adresseerbaar_object_id": "0363010000599756", - "subtype": "verblijfsobject", - "centroid": [4.834586581980725, 52.372950494299445], - "subtype_id": "03630000599756", - "_display": "Burgemeester Röellstraat 1" - }, - { - "_links": { - "self": { - "href": "https://api.data.amsterdam.nl/bag/verblijfsobject/0363010000599785/" - } - }, - "type": "verblijfsobject", - "dataset": "nummeraanduiding", - "adres": "Burgemeester Röellstraat 10", - "postcode": "1064BM", - "straatnaam": "Burgemeester Röellstraat", - "huisnummer": 10, - "toevoeging": "10", - "bag_huisletter": "", - "bag_toevoeging": "", - "woonplaats": "Amsterdam", - "hoofdadres": true, - "status": [ - { - "code": "16", - "omschrijving": "Naamgeving uitgegeven" - } - ], - "landelijk_id": "0363200000062116", - "vbo_status": [ - { - "code": "21", - "omschrijving": "Verblijfsobject in gebruik" - } - ], - "adresseerbaar_object_id": "0363010000599785", - "subtype": "verblijfsobject", - "centroid": [4.834578743747967, 52.3735795998216], - "subtype_id": "03630000599785", - "_display": "Burgemeester Röellstraat 10" - }, - { - "_links": { - "self": { - "href": "https://api.data.amsterdam.nl/bag/v1.1/verblijfsobject/0363010000664346/" - } - }, - "type": "verblijfsobject", - "dataset": "v11_nummeraanduiding", - "adres": "Herengracht 23-1", - "postcode": "1015BA", - "straatnaam": "Herengracht", - "straatnaam_no_ws": "Herengracht", - "huisnummer": 23, - "toevoeging": "23 1", - "bag_huisletter": "", - "bag_toevoeging": "1", - "woonplaats": "Amsterdam", - "type_adres": "Hoofdadres", - "status": "Naamgeving uitgegeven", - "landelijk_id": "0363200000126446", - "vbo_status": "Verblijfsobject in gebruik", - "adresseerbaar_object_id": "0363010000664346", - "subtype": "verblijfsobject", - "centroid": [4.891968036478453, 52.37873183842775], - "subtype_id": "0363010000664346", - "_display": "Herengracht 23-1" - }, - { - "_links": { - "self": { - "href": "https://api.data.amsterdam.nl/bag/v1.1/verblijfsobject/0457010000002241/" - } - }, - "type": "verblijfsobject", - "dataset": "v11_nummeraanduiding", - "adres": "Herengracht 23", - "postcode": "1382AG", - "straatnaam": "Herengracht", - "straatnaam_no_ws": "Herengracht", - "huisnummer": 23, - "toevoeging": "23", - "bag_huisletter": "", - "bag_toevoeging": "", - "woonplaats": "Weesp", - "type_adres": "Hoofdadres", - "status": "Naamgeving uitgegeven", - "landelijk_id": "0457200000201571", - "vbo_status": "Verblijfsobject in gebruik", - "adresseerbaar_object_id": "0457010000002241", - "subtype": "verblijfsobject", - "centroid": [5.039817231849981, 52.30885683238395], - "subtype_id": "0457010000002241", - "_display": "Herengracht 23 (Weesp)" - }, - { - "_links": { - "self": { - "href": "https://api.data.amsterdam.nl/bag/v1.1/verblijfsobject/0363010000755560/" - } - }, - "type": "verblijfsobject", - "dataset": "v11_nummeraanduiding", - "adres": "Nieuwe Herengracht 23-1", - "postcode": "1011RL", - "straatnaam": "Nieuwe Herengracht", - "straatnaam_no_ws": "Nieuwe Herengracht", - "huisnummer": 23, - "toevoeging": "23 1", - "bag_huisletter": "", - "bag_toevoeging": "1", - "woonplaats": "Amsterdam", - "type_adres": "Hoofdadres", - "status": "Naamgeving uitgegeven", - "landelijk_id": "0363200000215998", - "vbo_status": "Verblijfsobject in gebruik", - "adresseerbaar_object_id": "0363010000755560", - "subtype": "verblijfsobject", - "centroid": [4.902795334609859, 52.36631966746123], - "subtype_id": "0363010000755560", - "_display": "Nieuwe Herengracht 23-1" - } - ] -} diff --git a/src/client/components/LocationModal/LocationModal.tsx b/src/client/components/LocationModal/LocationModal.tsx index 002ca50858..59bbda1a51 100644 --- a/src/client/components/LocationModal/LocationModal.tsx +++ b/src/client/components/LocationModal/LocationModal.tsx @@ -6,6 +6,7 @@ import { LatLngLiteral } from 'leaflet'; import styles from './LocationModal.module.scss'; import { LOCATION_ZOOM } from '../../../universal/config/myarea-datasets'; +import { PUBLIC_API_URLS } from '../../../universal/config/url'; import { extractAddress, getLatLngWithAddress, @@ -13,7 +14,11 @@ import { isLocatedInWeesp, LatLngWithAddress, } from '../../../universal/helpers/bag'; -import { BAGSearchResult, BAGSourceData } from '../../../universal/types/bag'; +import { + BAGAdreseerbaarObject, + BAGQueryParams, + BAGSourceData, +} from '../../../universal/types/bag'; import { Modal } from '../../components'; import { BaseLayerType } from '../../components/MyArea/Map/BaseLayerToggle'; import MyAreaLoader from '../../components/MyArea/MyAreaLoader'; @@ -23,20 +28,25 @@ import { MapLocationMarker } from '../MyArea/MyArea.hooks'; function transformBagSearchResultsResponse( response: BAGSourceData, - querySearchAddress: string, + querySearchAddress: BAGQueryParams, isWeesp: boolean ): LatLngWithAddress[] | null { - const results = response?.results ?? []; + const adresseerbareObjecten = response?._embedded.adresseerbareobjecten ?? []; // Try to get exact match - const latlng = getLatLonByAddress(results, querySearchAddress, isWeesp); - if (latlng && results.length === 1) { - return [latlng]; + const latlngWithAddress = getLatLonByAddress( + adresseerbareObjecten, + querySearchAddress, + isWeesp + ); + + if (latlngWithAddress && adresseerbareObjecten.length === 1) { + return [latlngWithAddress]; } // No exact match, return all results - if (results.length) { - return results.map((result: BAGSearchResult) => + if (adresseerbareObjecten.length) { + return adresseerbareObjecten.map((result: BAGAdreseerbaarObject) => getLatLngWithAddress(result) ); } @@ -91,10 +101,10 @@ export function LocationModal({ if (isLocationModalOpen) { const querySearchAddress = extractAddress(address); const isWeesp = isLocatedInWeesp(address); - // Updates bagApi state fetchBag({ - url: `https://api.data.amsterdam.nl/atlas/search/adres/?features=2&q=${querySearchAddress}`, + url: PUBLIC_API_URLS.BAG_ADRESSEERBARE_OBJECTEN, + params: querySearchAddress, transformResponse(responseData: BAGSourceData) { const latlngResults = transformBagSearchResultsResponse( responseData, diff --git a/src/server/config/source-api.ts b/src/server/config/source-api.ts index e188d1d948..430c824376 100644 --- a/src/server/config/source-api.ts +++ b/src/server/config/source-api.ts @@ -5,6 +5,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { ONE_HOUR_MS, ONE_MINUTE_MS, ONE_SECOND_MS } from './app'; import { IS_TAP } from '../../universal/config/env'; import { FeatureToggle } from '../../universal/config/feature-toggles'; +import { PUBLIC_API_URLS } from '../../universal/config/url'; import { getCert } from '../helpers/cert'; import { getFromEnv } from '../helpers/env'; @@ -228,7 +229,7 @@ export const ApiConfig: ApiDataRequestConfig = { }), }, BAG: { - url: `https://api.data.amsterdam.nl/atlas/search/adres/`, + url: PUBLIC_API_URLS.BAG_ADRESSEERBARE_OBJECTEN, }, ERFPACHTv2: { url: getFromEnv('BFF_ERFPACHT_API_URL'), diff --git a/src/server/services/bag.test.ts b/src/server/services/bag.test.ts index 71abbb138b..77bd91825e 100644 --- a/src/server/services/bag.test.ts +++ b/src/server/services/bag.test.ts @@ -1,48 +1,65 @@ import nock from 'nock'; -import { describe, expect, it } from 'vitest'; import { fetchBAG } from './bag'; -import bagData from '../../../mocks/fixtures/bag.json'; -import { jsonCopy } from '../../universal/helpers/utils'; import { Adres } from '../../universal/types'; -describe('BAG service', () => { - const DUMMY_RESPONSE = jsonCopy(bagData); +const REQUEST_ID = 'x'; +const ADDRESS = { + straatnaam: 'straatje', + huisnummer: 25, + woonplaatsNaam: 'Amsterdam', +} as unknown as Adres; - it('Bag api should reply correctly', async () => { - nock('https://api.data.amsterdam.nl') - .get('/atlas/search/adres/?q=straatje 25&features=2') - .reply(200, DUMMY_RESPONSE); +// This is only a section of the mock data from the source. +const BAG_MOCK_DATA = { + _embedded: { + adresseerbareobjecten: [ + { + identificatie: '0363200012145295', + huisnummer: 1, + huisletter: null, + huisnummertoevoeging: null, + postcode: '1011PN', + woonplaatsNaam: 'Amsterdam', + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point', + coordinates: [4.9001655, 52.3676456], + }, + }, + ], + }, +}; - const address = { - straatnaam: 'straatje', - huisnummer: 25, - woonplaatsNaam: 'Amsterdam', - } as unknown as Adres; +function setupNockResponse(reply: number, response?: object) { + nock('https://api.data.amsterdam.nl') + .get( + '/v1/benkagg/adresseerbareobjecten/?openbareruimteNaam=straatje&huisnummer=25' + ) + .reply(reply, response); +} - const rs = await fetchBAG('x', address); +describe('BAG service', () => { + setupNockResponse(200, BAG_MOCK_DATA); - expect(rs).toStrictEqual({ + test('Bag api should reply correctly', async () => { + const response = await fetchBAG(REQUEST_ID, ADDRESS); + + expect(response).toStrictEqual({ status: 'OK', content: { - address, - bagNummeraanduidingId: null, - latlng: null, + address: ADDRESS, + bagNummeraanduidingId: '0363200012145295', + latlng: { + lat: 52.3676456, + lng: 4.9001655, + }, }, }); }); - it('Bag api should fail correctly', async () => { - nock('http://api.data.amsterdam.nl') - .get('/bag', { params: { q: 'undefined' } }) - .reply(500); - // Request non-existing mock url - const rs = await fetchBAG('x', {} as any); - - expect(rs).toStrictEqual({ - status: 'ERROR', - message: 'Kon geen correct zoek adres opmaken.', - content: null, - }); + test('No data in response', async () => { + setupNockResponse(200, {}); + const response = await fetchBAG(REQUEST_ID, ADDRESS); + expect(response).toStrictEqual({ status: 'OK', content: null }); }); }); diff --git a/src/server/services/bag.ts b/src/server/services/bag.ts index f89689fe79..2f0a1f6c86 100644 --- a/src/server/services/bag.ts +++ b/src/server/services/bag.ts @@ -1,12 +1,9 @@ import { LatLngLiteral } from 'leaflet'; -import { apiErrorResult } from '../../universal/helpers/api'; -import { - getMatchingBagResult, - getBagSearchAddress, - getLatLonByAddress, -} from '../../universal/helpers/bag'; +import { apiErrorResult, ApiResponse } from '../../universal/helpers/api'; +import { getLatLngCoordinates } from '../../universal/helpers/bag'; import { Adres } from '../../universal/types'; +import { BAGQueryParams } from '../../universal/types/bag'; import { getApiConfig } from '../helpers/source-api-helpers'; import { requestData } from '../helpers/source-api-request'; @@ -20,40 +17,36 @@ export interface BAGData { export async function fetchBAG( requestID: RequestID, sourceAddress: Adres | null -) { - if (!sourceAddress) { +): Promise> { + if (!sourceAddress?.straatnaam || !sourceAddress.huisnummer) { return apiErrorResult('Could not query BAG, no address supplied.', null); } - const searchAddress = getBagSearchAddress(sourceAddress); + const params: BAGQueryParams = { + openbareruimteNaam: sourceAddress.straatnaam, + huisnummer: sourceAddress.huisnummer, + huisletter: sourceAddress.huisletter || undefined, + }; - if (!searchAddress) { - return apiErrorResult(`Kon geen correct zoek adres opmaken.`, null); - } - - const params = { q: searchAddress, features: 2 }; // features=2 is een Feature flag zodat ook locaties in Weesp worden weergegeven. const config = getApiConfig('BAG', { params, - cacheKey: `${requestID}-${searchAddress}`, + cacheKey: `${requestID}-${sourceAddress.straatnaam}-${sourceAddress.huisnummer}${sourceAddress.huisletter}`, transformResponse: (responseData) => { - const isWeesp = sourceAddress.woonplaatsNaam === 'Weesp'; + const data = responseData._embedded?.adresseerbareobjecten; + if (!data || data.length < 1) { + return null; + } - const latlng = getLatLonByAddress( - responseData?.results, - searchAddress, - isWeesp - ); + // Multiple items can be found, but only the first we take as relevant. + const firstItem = data[0]; - const bagResult = getMatchingBagResult( - responseData?.results, - searchAddress, - isWeesp + const latlng = getLatLngCoordinates( + firstItem.adresseerbaarObjectPuntGeometrieWgs84.coordinates ); - return { latlng, address: sourceAddress, - bagNummeraanduidingId: bagResult?.landelijk_id ?? null, + bagNummeraanduidingId: firstItem.identificatie, }; }, }); diff --git a/src/server/services/brp.test.ts b/src/server/services/brp.test.ts index 6be701ed25..063427b402 100644 --- a/src/server/services/brp.test.ts +++ b/src/server/services/brp.test.ts @@ -1,7 +1,6 @@ import { transformBRPData, transformBRPNotifications } from './brp'; import brpData from '../../../mocks/fixtures/brp.json'; import { ApiSuccessResponse } from '../../universal/helpers/api'; -import { getBagSearchAddress } from '../../universal/helpers/bag'; import { getFullAddress } from '../../universal/helpers/brp'; import { BRPDataFromSource } from '../../universal/types/brp'; @@ -11,10 +10,6 @@ const { } = brpDataTyped; describe('BRP data api + transformation', () => { - it('should construct a bag search addresss', () => { - expect(getBagSearchAddress(adres)).toBe('Weesperstraat 113'); - }); - it('should construct a complete addresss', () => { expect( getFullAddress({ ...adres, huisletter: 'X', huisnummertoevoeging: 'h' }) diff --git a/src/server/services/my-locations.ts b/src/server/services/my-locations.ts index 5072d60582..c037dd82c6 100644 --- a/src/server/services/my-locations.ts +++ b/src/server/services/my-locations.ts @@ -23,10 +23,10 @@ async function fetchPrivate( if (BRP.status === 'OK') { if (isMokum(BRP.content)) { - const location = (await fetchBAG(requestID, BRP.content.adres))?.content; + const BAGLocation = (await fetchBAG(requestID, BRP.content.adres)) + ?.content; - // No BAG location found - if (!location?.latlng) { + if (!BAGLocation?.latlng) { return apiSuccessResult([ { latlng: { @@ -38,13 +38,11 @@ async function fetchPrivate( }, ]); } else { - // BAG Location found! return apiSuccessResult([ - Object.assign(location, { profileType: 'private' }), + Object.assign(BAGLocation, { profileType: 'private' }), ]); } } else { - // Not a Mokum address return apiSuccessResult([ { latlng: null, diff --git a/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.test.ts b/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.test.ts index d9d75cf306..f6d44d58de 100644 --- a/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.test.ts +++ b/src/server/services/toeristische-verhuur/toeristische-verhuur-powerbrowser-bb-vergunning.test.ts @@ -770,6 +770,10 @@ describe('B&B Vergunningen service', () => { }); describe('transformZaak', () => { + beforeEach(() => { + Mockdate.set('2023-01-01'); + }); + test('should transform zaak successfully', () => { const zaak: PBZaakRecord = { fmtCpn: diff --git a/src/universal/config/url.ts b/src/universal/config/url.ts new file mode 100644 index 0000000000..aca6451e27 --- /dev/null +++ b/src/universal/config/url.ts @@ -0,0 +1,5 @@ +/** A map of URLS that are shared between backend and frontend */ +export const PUBLIC_API_URLS = { + BAG_ADRESSEERBARE_OBJECTEN: + 'https://api.data.amsterdam.nl/v1/benkagg/adresseerbareobjecten/', +}; diff --git a/src/universal/helpers/bag.test.ts b/src/universal/helpers/bag.test.ts index 32b79d1eca..be2ee438d3 100644 --- a/src/universal/helpers/bag.test.ts +++ b/src/universal/helpers/bag.test.ts @@ -1,135 +1,221 @@ -import { Adres } from '../types'; -import { - extractAddress, - getBagSearchAddress, - getLatLonByAddress, - isLocatedInWeesp, -} from './bag'; -import { BAGSourceData } from '../types/bag'; +import { extractAddress, getLatLonByAddress, isLocatedInWeesp } from './bag'; +import { BAGQueryParams, BAGSourceData } from '../types/bag'; describe('getLatLonByAddress', () => { - const weesp = 'Herengracht 23'; - const amsterdam = 'Herengracht 23-1'; + const weesp: BAGQueryParams = { + openbareruimteNaam: 'Herengracht', + huisnummer: 23, + }; + const amsterdam: BAGQueryParams = { + openbareruimteNaam: 'Herengracht', + huisnummer: 23, + huisnummertoevoeging: '1', + }; const response: BAGSourceData = { - results: [ - { - adres: 'Herengracht 23-1', - centroid: [4.891968036478453, 52.37873183842775], - woonplaats: 'Amsterdam', - landelijk_id: 'xxx1', - }, - { - adres: 'Herengracht 23-2', - centroid: [4.891968036478453, 52.37873183842775], - woonplaats: 'Amsterdam', - landelijk_id: 'xxx2', - }, - { - adres: 'Herengracht 23-H', - centroid: [4.891968036478453, 52.37873183842775], - woonplaats: 'Amsterdam', - landelijk_id: 'xxx3', - }, - { - adres: 'Herengracht 23', - centroid: [5.039817231849981, 52.30885683238395], - woonplaats: 'Weesp', - landelijk_id: 'xxx4', - }, - { - adres: 'Herengracht 231a', - centroid: [5.03863916842061, 52.30886128545404], - woonplaats: 'Weesp', - landelijk_id: 'xxx5', - }, - { - adres: 'Nieuwe Herengracht 23-1', - centroid: [4.902795334609859, 52.36631966746123], - woonplaats: 'Amsterdam', - landelijk_id: 'xxx6', - }, - ], + _embedded: { + adresseerbareobjecten: [ + { + openbareruimteNaam: 'Herengracht', + huisnummer: 23, + huisletter: null, + huisnummertoevoeging: '1', + postcode: '1015BA', + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point', + coordinates: [5.0, 50.0], + }, + woonplaatsNaam: 'Amsterdam', + identificatie: 'xxx1', + }, + { + openbareruimteNaam: 'Herengracht', + huisnummer: 23, + huisletter: null, + huisnummertoevoeging: '2', + postcode: '1015BA', + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point', + coordinates: [6.0, 50.0], + }, + woonplaatsNaam: 'Amsterdam', + identificatie: 'xxx2', + }, + { + openbareruimteNaam: 'Herengracht', + huisnummer: 23, + huisletter: null, + huisnummertoevoeging: 'H', + postcode: '1015BA', + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point', + coordinates: [7.0, 50.0], + }, + woonplaatsNaam: 'Amsterdam', + identificatie: 'xxx3', + }, + { + openbareruimteNaam: 'Herengracht', + huisnummer: 23, + huisletter: null, + huisnummertoevoeging: null, + postcode: '1015BA', + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point', + coordinates: [8.0, 50.0], + }, + woonplaatsNaam: 'Weesp', + identificatie: 'xxx4', + }, + { + openbareruimteNaam: 'Herengracht', + huisnummer: 231, + huisletter: null, + huisnummertoevoeging: 'a', + postcode: '1015BA', + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point', + coordinates: [9.0, 50.0], + }, + woonplaatsNaam: 'Weesp', + identificatie: 'xxx5', + }, + { + openbareruimteNaam: 'Nieuwe Herengracht', + huisnummer: 23, + huisletter: null, + huisnummertoevoeging: '1', + postcode: '1015BA', + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point', + coordinates: [10.0, 50.0], + }, + woonplaatsNaam: 'Amsterdam', + identificatie: 'xxx6', + }, + ], + }, }; - test('Amsterdam', () => { - expect(getLatLonByAddress(response?.results, amsterdam, false)).toEqual({ + test('Found match in Amsterdam', () => { + expect( + getLatLonByAddress( + response._embedded.adresseerbareobjecten, + amsterdam, + false + ) + ).toEqual({ address: 'Herengracht 23-1', - lat: 52.37873183842775, - lng: 4.891968036478453, + lat: 50.0, + lng: 5.0, }); }); - test('Weesp', () => { - expect(getLatLonByAddress(response?.results, weesp, true)).toEqual({ + test('Found match in Weesp', () => { + expect( + getLatLonByAddress(response._embedded.adresseerbareobjecten, weesp, true) + ).toEqual({ address: 'Herengracht 23', - lat: 52.30885683238395, - lng: 5.039817231849981, + lat: 50.0, + lng: 8.0, }); }); - test('extractAddress', () => { - expect(extractAddress('Herengracht 23-1, 1015BA, Amsterdam _ ; ,')).toBe( - 'Herengracht 23-1' - ); - - expect( - extractAddress('Burgemeester Röellstraat 44, 1015BA, Amsterdam _ ; ,') - ).toBe('Burgemeester Röellstraat 44'); - }); + describe('extractAddress tests', () => { + test('Throws with bad input', () => { + expect(() => extractAddress('')).toThrowError(); + expect(() => extractAddress('Only Streetname')).toThrowError(); + }); - test('isWeesp', () => { - expect(isLocatedInWeesp('Weesperstraat 113 Amsterdam')).toBe(false); - expect(isLocatedInWeesp('Herengracht 23 Weesp')).toBe(true); + test('Short streetname with single digit', () => { + expect(extractAddress('Amstel 1')).toStrictEqual({ + openbareruimteNaam: 'Amstel', + huisnummer: 1, + huisnummertoevoeging: undefined, + huisletter: undefined, + }); + }); - // This is an exception. Currently no addresses including the word "amsterdam" exist in Weesp so for now this function is sufficient. - expect(isLocatedInWeesp('Amsterdamse straatweg 999 Weesp')).toBe(false); - }); + test('Long streetname with simple number', () => { + expect(extractAddress('Burgemeester Röellstraat 44')).toStrictEqual({ + openbareruimteNaam: 'Burgemeester Röellstraat', + huisnummer: 44, + huisnummertoevoeging: undefined, + huisletter: undefined, + }); + }); - test('getBagSearchAddress', () => { - expect( - getBagSearchAddress({ - straatnaam: 'Herengracht', - huisnummer: '23', - huisletter: null, - huisnummertoevoeging: null, - } as Adres) - ).toBe('Herengracht 23'); + test('huisnummertoevoeging extracted', () => { + expect(extractAddress('Herengracht 23-1')).toStrictEqual({ + openbareruimteNaam: 'Herengracht', + huisnummer: 23, + huisnummertoevoeging: '1', + huisletter: undefined, + }); + }); - expect( - getBagSearchAddress({ - straatnaam: 'Herengracht', - huisnummer: '23', - huisletter: null, + test('huisnummertoevoeging extracted with trailing comma', () => { + expect(extractAddress('Herengracht 23-1,')).toStrictEqual({ + openbareruimteNaam: 'Herengracht', + huisnummer: 23, huisnummertoevoeging: '1', - } as Adres) - ).toBe('Herengracht 23-1'); + huisletter: undefined, + }); + }); - expect( - getBagSearchAddress({ - straatnaam: 'Herengracht', - huisnummer: '23', - huisletter: null, + test('Letter extracted as toevoeging', () => { + expect(extractAddress('Insulindeweg 26A')).toStrictEqual({ + openbareruimteNaam: 'Insulindeweg', + huisnummer: 26, huisnummertoevoeging: 'A', - } as Adres) - ).toBe('Herengracht 23-A'); + huisletter: undefined, + }); + }); - expect( - getBagSearchAddress({ - straatnaam: 'Herengracht', - huisnummer: '23', - huisletter: 'C', - huisnummertoevoeging: null, - } as Adres) - ).toBe('Herengracht 23C'); + test('With leading apostrophe', () => { + expect(extractAddress("'t Dijkhuis 40")).toStrictEqual({ + openbareruimteNaam: "'t Dijkhuis", + huisnummer: 40, + huisnummertoevoeging: undefined, + huisletter: undefined, + }); + }); - expect( - getBagSearchAddress({ - straatnaam: 'Herengracht', - huisnummer: '23', - huisletter: 'C', - huisnummertoevoeging: '1', - } as Adres) - ).toBe('Herengracht 23C-1'); + test('With a slash in the address', () => { + expect(extractAddress('PATER V/D ELSENPLEIN 86')).toStrictEqual({ + openbareruimteNaam: 'PATER V/D ELSENPLEIN', + huisnummer: 86, + huisnummertoevoeging: undefined, + huisletter: undefined, + }); + }); + + test('With a dot in the address', () => { + expect(extractAddress('P/A ST. JACOBSLAAN 339')).toStrictEqual({ + openbareruimteNaam: 'P/A ST. JACOBSLAAN', + huisnummer: 339, + huisnummertoevoeging: undefined, + huisletter: undefined, + }); + }); + + test('Ignores postcal code, city name and random charcters', () => { + expect( + extractAddress('Straatnaam 1, 1015BA, Amsterdam _ ; ,') + ).toStrictEqual({ + openbareruimteNaam: 'Straatnaam', + huisnummer: 1, + huisnummertoevoeging: undefined, + huisletter: undefined, + }); + }); + }); + + test('Adres is located in Weesp', () => { + expect(isLocatedInWeesp('Weesperstraat 113 Amsterdam')).toBe(false); + expect(isLocatedInWeesp('Herengracht 23 Weesp')).toBe(true); + + // This is an exception. Currently no addresses including the word "amsterdam" exist in Weesp so for now this function is sufficient. + expect(isLocatedInWeesp('Amsterdamse straatweg 999 Weesp')).toBe(false); }); }); diff --git a/src/universal/helpers/bag.ts b/src/universal/helpers/bag.ts index 8c2a5384b9..b37cbb72e6 100644 --- a/src/universal/helpers/bag.ts +++ b/src/universal/helpers/bag.ts @@ -1,55 +1,127 @@ import { LatLngLiteral, LatLngTuple } from 'leaflet'; -import { BAGSearchResult, BAGSourceData } from '../types/bag'; -import { Adres } from '../types/brp'; - -// Quick and dirty see also: https://stackoverflow.com/a/68401047 -export function extractAddress(rawAddress: string) { - // Strip down to Street + Housenumber - const address = rawAddress - // Remove everything but alphanumeric, dash, dot and space - .replace(/[^0-9-\.\s\p{Script=Latin}+]/giu, '') - // Remove woonplaats - .replace(/(Amsterdam|Weesp)/gi, '') - // Remove postalcode - .replace(/([1-9][0-9]{3} ?(?!sa|sd|ss)[a-z]{2})/i, '') - .trim(); - - return address; +import { BAGQueryParams, BAGAdreseerbaarObject } from '../types/bag'; + +export function extractAddress(rawAddress: string): BAGQueryParams { + // Remove everything but alphanumeric, dash, dot, apostrophe and space. + const address = rawAddress.replace(/[^/'0-9-.\s\p{Script=Latin}+]/giu, ''); + + const words = []; + const s = address.split(' '); + + if (s.length < 2) { + throw Error( + `Address should consist of minimally two parts. A streetname and a housenumber. + Address: '${address}'` + ); + } + + let i = 0; + const onDigit = new RegExp(/\d/); + + for (; i < s.length; i++) { + const word = s[i]; + if (word[0].match(onDigit)) { + // The first housenumber found, so there are no more streetname words left. + break; + } + words.push(word); + } + + // We know now that we're past the street name so now we can index into the last identifying part. + const houseIdentifier = s[i]; + if (!houseIdentifier) { + throw Error( + `No houseIdentifier part, can't parse incomplete address: '${address}'` + ); + } + + const [huisnummer, huisnummertoevoeging] = + splitHuisnummerFromToevoeging(houseIdentifier); + + return { + openbareruimteNaam: words.join(' '), + huisnummer: parseInt(huisnummer), + huisnummertoevoeging, + // Leave out huisletter. This is used to look up a location on the map, + // and it's okay to show an approximate location. + huisletter: undefined, + }; +} + +function splitHuisnummerFromToevoeging( + s: string +): [string, string | undefined] { + const huisnummer = []; + const huisnummertoevoeging = []; + let i = 0; + + // Matches something like 1, 2-5 or 3F. + const matches = s.match(/(\d+)-?(\d*|\w*)?/); + if (!matches) { + throw Error( + `Match failed for housenumber and/or toevoeging. Input string: '${s}'` + ); + } + return [matches[1], matches[2]]; } export type BAGSearchAddress = string; export type LatLngWithAddress = LatLngLiteral & { address: string }; +/** Find a matching object in adresseerbareObjecten. + * + * A Match is a object where both the same adres as residency is found. + * It's possible for the same street name to exist in both Weesp and Amsterdam. + * So this prevents selecting the wrong one. + */ export function getMatchingBagResult( - results: BAGSourceData['results'] = [], - bagSearchAddress: BAGSearchAddress, + adresseerbareObjecten: BAGAdreseerbaarObject[] = [], + bagSearchAddress: BAGQueryParams, isWeesp: boolean ) { - const result1 = results.find((result) => { - const isWoonplaatsMatch = - result.woonplaats === (isWeesp ? 'Weesp' : 'Amsterdam'); - - const isAddressMatch = result.adres - .toLowerCase() - .includes(bagSearchAddress.toLowerCase()); - - return isWeesp ? isWoonplaatsMatch && isAddressMatch : isAddressMatch; - }); + const foundAdresseerbaarObject = adresseerbareObjecten.find( + (adresseerbaarObject) => { + const isWoonplaatsMatch = + adresseerbaarObject.woonplaatsNaam === + (isWeesp ? 'Weesp' : 'Amsterdam'); + + const isAddressMatch = + adresseerbaarObject.openbareruimteNaam === + bagSearchAddress.openbareruimteNaam && + adresseerbaarObject.huisnummer === bagSearchAddress.huisnummer && + adresseerbaarObject.huisletter === + (bagSearchAddress.huisletter ?? null); + + return isWeesp ? isWoonplaatsMatch && isAddressMatch : isAddressMatch; + } + ); - return result1 ?? null; + return foundAdresseerbaarObject ?? null; } export function getLatLngWithAddress( - result: BAGSearchResult + result: BAGAdreseerbaarObject ): LatLngWithAddress { return { - address: result.adres, - ...(getLatLngCoordinates(result.centroid) ?? null), + address: formatAddress(result), + ...(getLatLngCoordinates( + result.adresseerbaarObjectPuntGeometrieWgs84.coordinates + ) ?? null), }; } +function formatAddress(result: BAGAdreseerbaarObject): string { + if (result.huisletter) { + throw Error('Huisletter found but formatting not implemented.'); + } + if (result.huisnummertoevoeging) { + return `${result.openbareruimteNaam} ${result.huisnummer}-${result.huisnummertoevoeging}`; + } + return `${result.openbareruimteNaam} ${result.huisnummer}`; +} + export function getLatLngCoordinates(centroid: LatLngTuple) { // Forced coordinates to be in the right order // Using Amsterdam lat/lng 52.xxxxxx, 4.xxxxxx @@ -60,36 +132,27 @@ export function getLatLngCoordinates(centroid: LatLngTuple) { } export function getLatLonByAddress( - results: BAGSourceData['results'] = [], - bagSearchAddress: BAGSearchAddress, + adresseerbareObjecten: BAGAdreseerbaarObject[] = [], + bagSearchAddress: BAGQueryParams, isWeesp: boolean ): LatLngWithAddress | null { - if (results.length) { - const result1 = getMatchingBagResult(results, bagSearchAddress, isWeesp); - if (result1 && result1.adres && result1.centroid) { - return getLatLngWithAddress(result1); + if (adresseerbareObjecten.length) { + const result = getMatchingBagResult( + adresseerbareObjecten, + bagSearchAddress, + isWeesp + ); + if ( + result && + result.openbareruimteNaam && + result.adresseerbaarObjectPuntGeometrieWgs84.coordinates + ) { + return getLatLngWithAddress(result); } } return null; } -export function getBagSearchAddress(adres: Adres): BAGSearchAddress | null { - let bagZoekAdres = - adres.straatnaam && adres.huisnummer - ? `${adres.straatnaam} ${adres.huisnummer}`.trim() - : null; - - if (bagZoekAdres && adres.huisletter) { - bagZoekAdres += `${adres.huisletter}`; // Bijvoorbeeld Herengracht 50C - } - - if (bagZoekAdres && adres.huisnummertoevoeging) { - bagZoekAdres += `-${adres.huisnummertoevoeging}`; // Bijvoorbeeld Da Costakade 50-1 - } - - return bagZoekAdres; -} - export function isLocatedInWeesp(address: string) { const lAddress = address.toLowerCase(); // NOTE: Currently no addresses including "amsterdam" are present in Weesp. So: Amsterdamse plein or Amsterdamse straatweg do not exist. diff --git a/src/universal/types/bag.ts b/src/universal/types/bag.ts index 014af40b1f..cb16d3fa79 100644 --- a/src/universal/types/bag.ts +++ b/src/universal/types/bag.ts @@ -1,13 +1,33 @@ -import { LatLngTuple } from 'leaflet'; - -export type BAGSearchResult = { - centroid: LatLngTuple; - adres: string; - woonplaats: string; - landelijk_id: string | null; - [key: string]: unknown; +/** An incomplete slice of a BAG adresseerbaar object. + * That is because not all fields are used. + */ +export type BAGAdreseerbaarObject = { + identificatie: string; + huisnummer: number; + huisletter: string | null; + huisnummertoevoeging: string | null; + postcode: string; + woonplaatsNaam: string; + openbareruimteNaam: string; // Also know as straatnaam. + adresseerbaarObjectPuntGeometrieWgs84: { + type: 'Point'; + coordinates: [number, number]; // In long lat order. (lowest number first) + }; }; +/** An incomplete slice of a adreseerbare object response. + * That is because not all fields are used. + */ export interface BAGSourceData { - results: BAGSearchResult[]; + _embedded: { + adresseerbareobjecten: BAGAdreseerbaarObject[]; + }; } + +/** Query Parameters for doing Axios requests to BAG_ADRESSEERBARE_OBJECTEN. */ +export type BAGQueryParams = { + openbareruimteNaam: string; + huisnummer: T; + huisletter?: string; + huisnummertoevoeging?: string; +};