implement address search and reverse geocoding APIs #5

Merged
reloia merged 3 commits from address-autocomplete into master 2026-06-15 13:27:00 +00:00
5 changed files with 15 additions and 106 deletions
Showing only changes of commit e7219c84ab - Show all commits
+10 -20
View File
@@ -3,6 +3,7 @@ import type { Category, CreateReportRequest } from '~/types/api'
import type { LeafletMouseEvent, Map as LeafletMap, Marker as LeafletMarker } from 'leaflet' import type { LeafletMouseEvent, Map as LeafletMap, Marker as LeafletMarker } from 'leaflet'
import { onMounted, onUnmounted, ref, watch } from 'vue' import { onMounted, onUnmounted, ref, watch } from 'vue'
import { BARI_BOUNDS, BARI_CENTER, isWithinBariBounds } from '~/utils/bari' import { BARI_BOUNDS, BARI_CENTER, isWithinBariBounds } from '~/utils/bari'
import { reverseGeocode, searchAddresses } from '~/utils/geocoding'
interface Props { interface Props {
isLoading?: boolean isLoading?: boolean
@@ -19,14 +20,6 @@ interface GeocodingSuggestion {
longitude: number longitude: number
} }
interface SearchAddressesResponse {
suggestions: GeocodingSuggestion[]
}
interface ReverseGeocodeResponse {
address: string
}
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isLoading: false, isLoading: false,
categories: () => [] categories: () => []
@@ -123,18 +116,20 @@ const fetchAddressSuggestions = async (query: string) => {
addressSearchError.value = '' addressSearchError.value = ''
try { try {
const response = await $fetch<SearchAddressesResponse>('/api/geocoding/search', { if (requestId !== addressSearchRequestId) {
query: { q: query } return
}) }
const suggestions = await searchAddresses(query)
if (requestId !== addressSearchRequestId) { if (requestId !== addressSearchRequestId) {
return return
} }
addressSuggestions.value = response.suggestions addressSuggestions.value = suggestions
showAddressSuggestions.value = response.suggestions.length > 0 showAddressSuggestions.value = suggestions.length > 0
if (!response.suggestions.length) { if (!suggestions.length) {
addressSearchError.value = 'Nessun indirizzo trovato nel Comune di Bari.' addressSearchError.value = 'Nessun indirizzo trovato nel Comune di Bari.'
} }
} catch { } catch {
@@ -157,15 +152,11 @@ const resolveAddressFromCoordinates = async (lat: number, lng: number, fallbackA
addressSearchError.value = '' addressSearchError.value = ''
try { try {
const response = await $fetch<ReverseGeocodeResponse>('/api/geocoding/reverse', {
query: { lat, lng }
})
if (requestId !== reverseGeocodeRequestId) { if (requestId !== reverseGeocodeRequestId) {
return return
} }
const resolvedAddress = response.address?.trim() || fallbackAddress.trim() const resolvedAddress = (await reverseGeocode(lat, lng))?.trim() || fallbackAddress.trim()
if (resolvedAddress) { if (resolvedAddress) {
setAddressValue(resolvedAddress) setAddressValue(resolvedAddress)
@@ -694,4 +685,3 @@ textarea {
align-items: center; align-items: center;
} }
</style> </style>
@@ -1,3 +1,5 @@
import { BARI_BOUNDS, isWithinBariBounds } from '~/utils/bari'
interface NominatimAddress { interface NominatimAddress {
road?: string road?: string
house_number?: string house_number?: string
@@ -27,26 +29,8 @@ export interface GeocodingSuggestion {
} }
const NOMINATIM_BASE_URL = 'https://nominatim.openstreetmap.org' const NOMINATIM_BASE_URL = 'https://nominatim.openstreetmap.org'
const BARI_BOUNDS = {
north: 41.1696,
south: 41.0718,
east: 16.9521,
west: 16.7564
}
const NOMINATIM_HEADERS = {
'User-Agent': 'Visione Frontend/1.0 (https://visione.studentindipendenti.it)',
'Referer': 'https://visione.studentindipendenti.it/'
}
const bariViewbox = `${BARI_BOUNDS.west},${BARI_BOUNDS.north},${BARI_BOUNDS.east},${BARI_BOUNDS.south}` const bariViewbox = `${BARI_BOUNDS.west},${BARI_BOUNDS.north},${BARI_BOUNDS.east},${BARI_BOUNDS.south}`
const isWithinBariBounds = (latitude: number, longitude: number) =>
latitude >= BARI_BOUNDS.south &&
latitude <= BARI_BOUNDS.north &&
longitude >= BARI_BOUNDS.west &&
longitude <= BARI_BOUNDS.east
const compactAddress = (displayName: string, address?: NominatimAddress) => { const compactAddress = (displayName: string, address?: NominatimAddress) => {
if (!address) { if (!address) {
return displayName return displayName
@@ -91,9 +75,7 @@ export const searchAddresses = async (query: string, limit = 5) => {
limit: String(limit) limit: String(limit)
}) })
const results = await $fetch<NominatimResult[]>(`${NOMINATIM_BASE_URL}/search?${params.toString()}`, { const results = await $fetch<NominatimResult[]>(`${NOMINATIM_BASE_URL}/search?${params.toString()}`)
headers: NOMINATIM_HEADERS
})
return results return results
.map(toSuggestion) .map(toSuggestion)
@@ -111,9 +93,7 @@ export const reverseGeocode = async (latitude: number, longitude: number) => {
'accept-language': 'it' 'accept-language': 'it'
}) })
const result = await $fetch<NominatimResult | null>(`${NOMINATIM_BASE_URL}/reverse?${params.toString()}`, { const result = await $fetch<NominatimResult | null>(`${NOMINATIM_BASE_URL}/reverse?${params.toString()}`)
headers: NOMINATIM_HEADERS
})
if (!result) { if (!result) {
return '' return ''
+1 -23
View File
@@ -8,6 +8,7 @@ export default defineNuxtConfig({
'@nuxtjs/sitemap', '@nuxtjs/sitemap',
'@nuxtjs/robots' '@nuxtjs/robots'
], ],
ssr: false,
runtimeConfig: { runtimeConfig: {
public: { public: {
@@ -35,29 +36,6 @@ export default defineNuxtConfig({
] ]
}, },
nitro: {
devProxy: {
'/api/v1': {
target: 'https://bk.visione.studentindipendenti.it/api/v1',
changeOrigin: true,
headers: {
host: 'bk.visione.studentindipendenti.it',
origin: 'https://visione.studentindipendenti.it',
referer: 'https://visione.studentindipendenti.it/'
}
},
'/uploads': {
target: 'https://bk.visione.studentindipendenti.it/uploads',
changeOrigin: true,
headers: {
host: 'bk.visione.studentindipendenti.it',
origin: 'https://visione.studentindipendenti.it',
referer: 'https://visione.studentindipendenti.it/'
}
}
}
},
app: { app: {
head: { head: {
link: [ link: [
-19
View File
@@ -1,19 +0,0 @@
import { createError, getQuery } from 'h3'
import { reverseGeocode } from '../../utils/geocoding'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const latitude = typeof query.lat === 'string' ? Number.parseFloat(query.lat) : Number.NaN
const longitude = typeof query.lng === 'string' ? Number.parseFloat(query.lng) : Number.NaN
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid coordinates'
})
}
const address = await reverseGeocode(latitude, longitude)
return { address }
})
-20
View File
@@ -1,20 +0,0 @@
import { createError, getQuery } from 'h3'
import { searchAddresses } from '../../utils/geocoding'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const rawText = typeof query.q === 'string' ? query.q.trim() : ''
const rawLimit = typeof query.limit === 'string' ? Number.parseInt(query.limit, 10) : 5
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(rawLimit, 1), 8) : 5
if (rawText.length < 3) {
throw createError({
statusCode: 400,
statusMessage: 'Query too short'
})
}
const suggestions = await searchAddresses(rawText, limit)
return { suggestions }
})