Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9757271
feat: add nominatim support to addressSearch and reverseGeocoder
oeninghe-dataport Mar 23, 2026
41544aa
Merge remote-tracking branch 'origin/next' into vue3/add-nominatim-su…
oeninghe-dataport May 20, 2026
b944581
docs: use non-question sentences for attribute typedoc
oeninghe-dataport May 26, 2026
3221cd5
refactor(reverseGeocoder): simplify return object for nominatim
oeninghe-dataport May 26, 2026
6a58368
chore: remove TODO and model idea as feature request
oeninghe-dataport Jun 1, 2026
9cae6a1
refactor: replace functional implementation with simpler procedural
oeninghe-dataport Jun 1, 2026
169d26e
refactor: introduce generic transform util for geometries
oeninghe-dataport Jun 1, 2026
d19d95a
refactor: type response of nominatim feature search
oeninghe-dataport Jun 1, 2026
687b39c
test(snowbox): remove optional queryParameters for nominatim
oeninghe-dataport Jun 1, 2026
4aed172
Merge branch 'next' into vue3/add-nominatim-support
oeninghe-dataport Jun 2, 2026
5c87397
docs: fix typo for transformGeometry
oeninghe-dataport Jun 17, 2026
e3e9c13
Merge branch 'next' into vue3/add-nominatim-support
oeninghe-dataport Jul 10, 2026
b902a4f
fix(reverseGeocoder): allow reverse-geocoding WPS with differing EPSG
oeninghe-dataport Jul 10, 2026
ad67371
refactor(reverseGeocoder): type nominatim response
oeninghe-dataport Jul 13, 2026
20d34b6
refactor(reverseGeocoder): more intuitive syntax for typing fetch res…
oeninghe-dataport Jul 14, 2026
d312765
Merge branch 'next' into vue3/add-nominatim-support
oeninghe-dataport Jul 29, 2026
ca44a6b
refactor(reverseGeocoder): more verbosity about serviceEpsg
oeninghe-dataport Jul 30, 2026
f250d7f
refactor(reverseGeocoder): simplify local function signature
oeninghe-dataport Jul 30, 2026
de4b588
feat(reverseGeocoder): use geojson response instead of jsonv2
oeninghe-dataport Jul 31, 2026
f3dc637
docs(reverseGeocoder): use inter-linking for plugin options
oeninghe-dataport Jul 31, 2026
a5606c2
refactor(reverseGeocoder): be even more explicit on serviceEpsg
oeninghe-dataport Jul 31, 2026
59fe89a
refactor(reverseGeocoder): use more precise type for nominatim response
oeninghe-dataport Aug 3, 2026
8d99a2b
Merge branch 'next' into vue3/add-nominatim-support
oeninghe-dataport Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/iceberg/components/IcebergMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ watch(map, (map) => {
toZoomLevel: 7,
}),
pluginReverseGeocoder({
type: 'wps',
url: 'https://geodienste.hamburg.de/HH_WPS',
coordinateSources: [
{
Expand Down
12 changes: 11 additions & 1 deletion examples/snowbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
colorScheme,
startCenter: [565874, 5934140],
layers: [
// TODO: Add internalization to snowbox

Check warning on line 95 in examples/snowbox/index.js

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO: Add internalization to snowbox'
{
id: basemapId,
visibility: true,
Expand Down Expand Up @@ -359,7 +359,10 @@
addPlugin(
map,
pluginReverseGeocoder({
url: 'https://geodienste.hamburg.de/HH_WPS',
// type: 'wps',
// url: 'https://geodienste.hamburg.de/HH_WPS',
type: 'nominatim',
url: 'https://polar.dataport.de/nominatim/reverse',
coordinateSources: [
{
plugin: 'pins',
Expand Down Expand Up @@ -509,6 +512,7 @@
displayComponent: true,
layoutTag: 'TOP_LEFT',
searchMethods: [
/*
{
queryParameters: {
searchStreets: true,
Expand All @@ -517,6 +521,12 @@
type: 'mpapi',
url: 'https://geodienste.hamburg.de/HH_WFS_GAGES?service=WFS&request=GetFeature&version=2.0.0',
},
*/
{
type: 'nominatim',
url: 'https://polar.dataport.de/nominatim/search',
queryParameters: {},
Comment thread
oeninghe-dataport marked this conversation as resolved.
Outdated
},
],
minLength: 3,
waitMs: 300,
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function addPlugins(map: typeof PolarContainer, enabledPlugins: string[]) {
layoutTag: 'BOTTOM_LEFT',
}),
enabledPlugins.includes('reverseGeocoder') &&
ReverseGeocoder({ url: '' }),
ReverseGeocoder({ url: '', type: 'wps' }),
enabledPlugins.includes('scale') &&
Scale({
displayComponent: true,
Expand Down
62 changes: 62 additions & 0 deletions src/lib/getFeatures/nominatim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Point } from 'geojson'

import { transform as transformCoordinates } from 'ol/proj'

import type { PolarGeoJsonFeatureCollection } from '@/core'

import type { NominatimParameters } from './types'

export default async function (
signal: AbortSignal,
url: string,
inputValue: string,
queryParameters: NominatimParameters
): Promise<PolarGeoJsonFeatureCollection> {
const { epsg, maxFeatures, ...native } = queryParameters

const fetchUrl = new URL(url)
Object.entries({
...Object.fromEntries(
Object.entries(native).map(([key, value]) => [
key,
Array.isArray(value) ? value.join(',') : value,
])
),
...(maxFeatures ? { limit: maxFeatures } : {}),
q: inputValue,
format: 'geojson',
polygon_geojson: 0, // TODO: Setting this to 1 is currently not supported well by addressSearch plugin. The plugin expects a point geometry.

Check warning on line 28 in src/lib/getFeatures/nominatim.ts

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO: Setting this to 1 is currently not...'
Comment thread
dopenguin marked this conversation as resolved.
Outdated
polygon_threshold: 5,
}).forEach(([key, value]) => {
fetchUrl.searchParams.set(key, value.toString())
})
Comment thread
dopenguin marked this conversation as resolved.
Outdated

return await fetch(fetchUrl, {
signal,
})
.then((response) => response.json())
Comment thread
dopenguin marked this conversation as resolved.
Outdated
.then((featureCollection) => ({
...featureCollection,
features: featureCollection.features.map((feature) => ({
...feature,
title: feature.properties.display_name,
geometry: {
...feature.geometry,
coordinates:
epsg === 'EPSG:4326'
? feature.geometry.coordinates
: feature.geometry.type === 'Polygon'
? feature.geometry.coordinates.map((ring) =>
ring.map((coord) =>
transformCoordinates(coord, 'EPSG:4326', epsg)
)
)
Comment thread
dopenguin marked this conversation as resolved.
Outdated
: transformCoordinates(
(feature.geometry as Point).coordinates,
'EPSG:4326',
epsg
),
},
})),
}))
}
82 changes: 82 additions & 0 deletions src/lib/getFeatures/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,85 @@ export interface WfsParameters extends QueryParameters {
*/
useRightHandWildcard?: boolean
}

export interface NominatimParameters extends QueryParameters {
/**
* Include address details in result?
Comment thread
dopenguin marked this conversation as resolved.
Outdated
*/
addressdetails?: boolean

/**
* Name and/or type of POI.
*/
amenity?: string

/**
* Whether to consider the viewbox as exact instead of fuzzy filter.
*/
bounded?: boolean

/**
* City.
*/
city?: string

/**
* Country.
*/
country?: string

/**
* Country code (ISO 3166-1alpha2).
*/
countrycodes?: string[]

/**
* County.
*/
Comment thread
dopenguin marked this conversation as resolved.
county?: string

/**
* Include entrances to buildings in results?
Comment thread
dopenguin marked this conversation as resolved.
Outdated
*/
entrances?: boolean

/**
* Add extra tags to result?
Comment thread
dopenguin marked this conversation as resolved.
Outdated
*/
extratags?: boolean

/**
* Which feature types to include.
*/
featureType?: 'country' | 'state' | 'city' | 'settlement'

/**
* Which type(s) of results to return.
*/
layer?: ('address' | 'poi' | 'railway' | 'natural' | 'manmade')[]

/**
* Include more names in alternate languages?
Comment thread
dopenguin marked this conversation as resolved.
Outdated
*/
namedetails?: boolean

/**
* Postal code.
*/
postalcode?: string

/**
* State.
*/
state?: string
Comment thread
dopenguin marked this conversation as resolved.

/**
* House number and street name.
*/
street?: string

/**
* Viewbox to prefer for searching the given term.
*/
viewbox?: [number, number, number, number]
}
4 changes: 2 additions & 2 deletions src/plugins/addressSearch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export interface AddressSearchPluginOptions extends PluginOptions {

/** Possible search methods by type. */
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type SearchType = 'bkg' | 'wfs' | 'mpapi' | string
export type SearchType = 'bkg' | 'wfs' | 'mpapi' | 'nominatim' | string

export type SearchDisplayMode = 'mixed' | 'categorized'

Expand Down Expand Up @@ -177,7 +177,7 @@ export interface SearchMethodConfiguration {

/**
* The object further describes details for the search request.
* Its contents vary by service type, see {@link BKGParameters}, {@link MpapiParameters} or {@link WfsParameters}.
* Its contents vary by service type, see {@link BKGParameters}, {@link MpapiParameters}, {@link WfsParameters} or {@link NominatimParameters}.
*/
queryParameters?: QueryParameters

Expand Down
3 changes: 2 additions & 1 deletion src/plugins/addressSearch/utils/methodContainer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import bkg from '@/lib/getFeatures/bkg'
import mpapi from '@/lib/getFeatures/mpapi'
import nominatim from '@/lib/getFeatures/nominatim'
import { getWfsFeatures } from '@/lib/getFeatures/wfs'

import type { SearchMethodFunction } from '../types'

export function getMethodContainer() {
const methods = { bkg, mpapi, wfs: getWfsFeatures }
const methods = { bkg, mpapi, nominatim, wfs: getWfsFeatures }

return {
registerSearchMethods: (
Expand Down
22 changes: 15 additions & 7 deletions src/plugins/reverseGeocoder/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
type ReverseGeocoderFeature,
type ReverseGeocoderPluginOptions,
} from './types'
import { reverseGeocode as reverseGeocodeUtil } from './utils/reverseGeocode'
import { reverseGeocodeNominatim } from './utils/reverseGeocodeNominatim'
import { reverseGeocodeWps } from './utils/reverseGeocodeWps'

/* eslint-disable tsdoc/syntax */
/**
Expand Down Expand Up @@ -77,9 +78,14 @@ export const useReverseGeocoderStore = defineStore(
abortController.value = new AbortController()
const signal = toRaw(abortController.value.signal)
try {
const reverseGeocodeUtil = {
wps: reverseGeocodeWps,
nominatim: reverseGeocodeNominatim,
}[configuration.value.type]
const feature = await reverseGeocodeUtil(
configuration.value.url,
coordinate,
coreStore.configuration.epsg,
signal
)
if (configuration.value.addressTarget) {
Expand Down Expand Up @@ -129,20 +135,20 @@ if (import.meta.vitest) {
const { createPinia, setActivePinia } = await import('pinia')
const { reactive } = await import('vue')
const useCoreStoreFile = await import('@/core/stores')
const reverseGeocodeUtilFile = await import('./utils/reverseGeocode')
const reverseGeocodeUtilFile = await import('./utils/reverseGeocodeWps')
const indicateLoadingFile = await import('@/lib/indicateLoading')

/* eslint-disable no-empty-pattern */
const test = _test.extend<{
reverseGeocodeUtil: Mock<typeof reverseGeocodeUtil>
reverseGeocodeUtil: Mock<typeof reverseGeocodeWps>
indicateLoading: Mock<typeof indicateLoading>
coreStore: Reactive<Record<string, unknown>>
store: ReturnType<typeof useReverseGeocoderStore>
}>({
reverseGeocodeUtil: [
async ({}, use) => {
const reverseGeocodeUtil = vi
.spyOn(reverseGeocodeUtilFile, 'reverseGeocode')
.spyOn(reverseGeocodeUtilFile, 'reverseGeocodeWps')
.mockResolvedValue(null as unknown as ReverseGeocoderFeature)
await use(reverseGeocodeUtil)
},
Expand All @@ -167,7 +173,9 @@ if (import.meta.vitest) {
}
const coreStore = reactive({
configuration: {
epsg: 'EPSG:25832',
[PluginId]: {
type: 'wps',
url: 'https://wps.example',
coordinateSources: [{ plugin: 'pins', key: 'coordinate' }],
addressTarget: { key: 'addressTarget' },
Expand Down Expand Up @@ -211,9 +219,10 @@ if (import.meta.vitest) {
}
pluginStore.pins.coordinate = [1, 2]
await new Promise((resolve) => setTimeout(resolve))
expect(reverseGeocodeUtil).toHaveBeenCalledWith(
expect(reverseGeocodeUtil).toHaveBeenCalledExactlyOnceWith(
'https://wps.example',
[1, 2],
'EPSG:25832',
store.abortController?.signal
)
})
Expand All @@ -228,8 +237,7 @@ if (import.meta.vitest) {
feature as unknown as ReverseGeocoderFeature
)
await store.reverseGeocode([3, 4])
expect(coreStore.addressTarget).toHaveBeenCalledOnce()
expect(coreStore.addressTarget).toHaveBeenCalledWith(feature)
expect(coreStore.addressTarget).toHaveBeenCalledExactlyOnceWith(feature)
})

test('zooms to input coordinate', async ({
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/reverseGeocoder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const PluginId = 'reverseGeocoder'
* Plugin options for reverse geocoder plugin.
*/
export interface ReverseGeocoderPluginOptions extends PluginOptions {
/**
* Type of reverse geocoding service.
*/
type: 'wps' | 'nominatim'

/**
* URL of a WPS service to use for reverse geocoding.
*/
Expand Down
53 changes: 53 additions & 0 deletions src/plugins/reverseGeocoder/utils/reverseGeocodeNominatim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { transform as transformCoordinate } from 'ol/proj'

import type { ReverseGeocoderFeature } from '../types'

export async function reverseGeocodeNominatim(
url: string,
coordinate: [number, number],
epsg: string,
signal: AbortSignal
): Promise<ReverseGeocoderFeature> {
const searchCoordinate = transformCoordinate(
coordinate,
epsg,
'EPSG:4326'
) as [number, number]

const fetchUrl = new URL(url)
fetchUrl.searchParams.set('lat', searchCoordinate[1].toString())
fetchUrl.searchParams.set('lon', searchCoordinate[0].toString())
fetchUrl.searchParams.set('format', 'jsonv2')

const result = await fetch(fetchUrl, { signal }).then((response) =>
response.json()
Comment thread
dopenguin marked this conversation as resolved.
Outdated
)

const resultObject: ReverseGeocoderFeature = {
type: 'reverse_geocoded',
title: [
[result.address.road, result.address.house_number]
.filter((x) => x)
.join(' '),
result.address.town || result.address.city || result.address.village,
]
.filter((x) => x)
.join(', '),
properties: result.properties,
Comment thread
oeninghe-dataport marked this conversation as resolved.
Outdated
geometry: {
// as clicked by user - usually want to keep this since user is pointing at something
coordinates: coordinate,
type: 'Point',
},
addressGeometry: {
// as returned by reverse geocoder
coordinates: transformCoordinate(
[result.lon, result.lat],
'EPSG:4326',
epsg
),
type: 'Point',
},
}
return resultObject
Comment thread
oeninghe-dataport marked this conversation as resolved.
Outdated
}
Loading
Loading