{/* Description */}
diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts
index cdcac563..5251b244 100644
--- a/client/src/i18n/translations/ar.ts
+++ b/client/src/i18n/translations/ar.ts
@@ -855,6 +855,7 @@ const ar: Record = {
'places.reservationNotesPlaceholder': 'ملاحظات الحجز، رقم التأكيد...',
'places.mapsSearchPlaceholder': 'ابحث عن أماكن...',
'places.mapsSearchError': 'فشل البحث عن المكان.',
+ 'places.loadingDetails': 'جارٍ تحميل تفاصيل المكان…',
'places.osmHint': 'يتم البحث عبر OpenStreetMap (بدون صور أو ساعات عمل أو تقييمات). أضف مفتاح Google API في الإعدادات للحصول على جميع التفاصيل.',
'places.osmActive': 'البحث عبر OpenStreetMap (بدون صور أو تقييمات أو ساعات عمل). أضف مفتاح Google API في الإعدادات لبيانات موسعة.',
'places.categoryCreateError': 'فشل إنشاء الفئة',
diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts
index d3b11e66..40d24bfe 100644
--- a/client/src/i18n/translations/br.ts
+++ b/client/src/i18n/translations/br.ts
@@ -837,6 +837,7 @@ const br: Record = {
'places.reservationNotesPlaceholder': 'Notas da reserva, código de confirmação...',
'places.mapsSearchPlaceholder': 'Buscar lugares...',
'places.mapsSearchError': 'Falha na busca de lugares.',
+ 'places.loadingDetails': 'Carregando detalhes do lugar…',
'places.osmHint': 'Busca via OpenStreetMap (sem fotos, horários ou avaliações). Adicione uma chave Google nas configurações para detalhes completos.',
'places.osmActive': 'Busca via OpenStreetMap (sem fotos, avaliações ou horário de funcionamento). Adicione uma chave Google em Configurações para mais dados.',
'places.categoryCreateError': 'Falha ao criar categoria',
diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts
index 130f9623..086de3bc 100644
--- a/client/src/i18n/translations/cs.ts
+++ b/client/src/i18n/translations/cs.ts
@@ -853,6 +853,7 @@ const cs: Record = {
'places.reservationNotesPlaceholder': 'Poznámky k rezervaci, potvrzovací kód...',
'places.mapsSearchPlaceholder': 'Hledat místa...',
'places.mapsSearchError': 'Hledání místa se nezdařilo.',
+ 'places.loadingDetails': 'Načítání podrobností místa…',
'places.osmHint': 'Používáte hledání přes OpenStreetMap (bez fotek a hodnocení). Pro plné detaily přidejte Google API klíč v nastavení.',
'places.osmActive': 'Hledání přes OpenStreetMap.',
'places.categoryCreateError': 'Nepodařilo se vytvořit kategorii',
diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts
index c9ebf453..811c7682 100644
--- a/client/src/i18n/translations/de.ts
+++ b/client/src/i18n/translations/de.ts
@@ -853,6 +853,7 @@ const de: Record = {
'places.reservationNotesPlaceholder': 'Reservierungsnotizen, Bestätigungsnummer...',
'places.mapsSearchPlaceholder': 'Ortssuche...',
'places.mapsSearchError': 'Ortssuche fehlgeschlagen.',
+ 'places.loadingDetails': 'Ortsdetails werden geladen…',
'places.osmHint': 'OpenStreetMap-Suche aktiv (ohne Bilder, Öffnungszeiten, Bewertungen). Für erweiterte Daten Google API Key in den Einstellungen hinterlegen.',
'places.osmActive': 'Suche via OpenStreetMap (ohne Bilder, Bewertungen & Öffnungszeiten). Google API Key in den Einstellungen hinterlegen für erweiterte Daten.',
'places.categoryCreateError': 'Fehler beim Erstellen der Kategorie',
diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts
index 2cb895a1..ad1a4824 100644
--- a/client/src/i18n/translations/en.ts
+++ b/client/src/i18n/translations/en.ts
@@ -872,6 +872,7 @@ const en: Record = {
'places.reservationNotesPlaceholder': 'Reservation notes, confirmation number...',
'places.mapsSearchPlaceholder': 'Search places...',
'places.mapsSearchError': 'Place search failed.',
+ 'places.loadingDetails': 'Loading place details…',
'places.osmHint': 'Using OpenStreetMap search (no photos, opening hours, or ratings). Add a Google API key in settings for full details.',
'places.osmActive': 'Search via OpenStreetMap (no photos, ratings or opening hours). Add a Google API key in Settings for enhanced data.',
'places.categoryCreateError': 'Failed to create category',
diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts
index 41219432..1ce404e7 100644
--- a/client/src/i18n/translations/es.ts
+++ b/client/src/i18n/translations/es.ts
@@ -828,6 +828,7 @@ const es: Record = {
'places.reservationNotesPlaceholder': 'Notas de reserva, número de confirmación...',
'places.mapsSearchPlaceholder': 'Buscar lugares...',
'places.mapsSearchError': 'La búsqueda de lugares falló.',
+ 'places.loadingDetails': 'Cargando detalles del lugar…',
'places.osmHint': 'Usando búsqueda con OpenStreetMap (sin fotos, horarios ni valoraciones). Añade una clave API de Google en Ajustes para obtener todos los detalles.',
'places.osmActive': 'Búsqueda mediante OpenStreetMap (sin fotos, valoraciones ni horarios). Añade una clave API de Google en Ajustes para datos ampliados.',
'places.categoryCreateError': 'No se pudo crear la categoría',
diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts
index cbc2e09c..c3825ab7 100644
--- a/client/src/i18n/translations/fr.ts
+++ b/client/src/i18n/translations/fr.ts
@@ -852,6 +852,7 @@ const fr: Record = {
'places.reservationNotesPlaceholder': 'Notes de réservation, numéro de confirmation…',
'places.mapsSearchPlaceholder': 'Rechercher des lieux…',
'places.mapsSearchError': 'La recherche de lieu a échoué.',
+ 'places.loadingDetails': 'Chargement des détails du lieu…',
'places.osmHint': 'Recherche via OpenStreetMap (pas de photos, horaires ni notes). Ajoutez une clé API Google dans les paramètres pour plus de détails.',
'places.osmActive': 'Recherche via OpenStreetMap (pas de photos, notes ni horaires). Ajoutez une clé API Google dans les paramètres pour des données enrichies.',
'places.categoryCreateError': 'Impossible de créer la catégorie',
diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts
index 40ce49a2..3046d2eb 100644
--- a/client/src/i18n/translations/hu.ts
+++ b/client/src/i18n/translations/hu.ts
@@ -853,6 +853,7 @@ const hu: Record = {
'places.reservationNotesPlaceholder': 'Foglalási jegyzetek, visszaigazolási szám...',
'places.mapsSearchPlaceholder': 'Helyek keresése...',
'places.mapsSearchError': 'Helykeresés sikertelen.',
+ 'places.loadingDetails': 'Hely adatainak betöltése…',
'places.osmHint': 'OpenStreetMap keresés aktív (képek, nyitvatartás és értékelések nélkül). Bővített adatokhoz add meg a Google API kulcsot a beállításokban.',
'places.osmActive': 'Keresés OpenStreetMap-en keresztül (képek, értékelések és nyitvatartás nélkül). Bővített adatokhoz add meg a Google API kulcsot a beállításokban.',
'places.categoryCreateError': 'Nem sikerült létrehozni a kategóriát',
diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts
index d5449ff8..60ffb247 100644
--- a/client/src/i18n/translations/it.ts
+++ b/client/src/i18n/translations/it.ts
@@ -853,6 +853,7 @@ const it: Record = {
'places.reservationNotesPlaceholder': 'Note della prenotazione, numero di conferma...',
'places.mapsSearchPlaceholder': 'Cerca luoghi...',
'places.mapsSearchError': 'Impossibile cercare i luoghi.',
+ 'places.loadingDetails': 'Caricamento dettagli del luogo…',
'places.osmHint': 'Uso della ricerca OpenStreetMap (senza foto, orari di apertura o valutazioni). Aggiungi una chiave API Google nelle impostazioni per i dettagli completi.',
'places.osmActive': 'Ricerca tramite OpenStreetMap (senza foto, valutazioni o orari di apertura). Aggiungi una chiave API Google nelle Impostazioni per dati avanzati.',
'places.categoryCreateError': 'Impossibile creare la categoria',
diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts
index 2e7495da..50c174dc 100644
--- a/client/src/i18n/translations/nl.ts
+++ b/client/src/i18n/translations/nl.ts
@@ -852,6 +852,7 @@ const nl: Record = {
'places.reservationNotesPlaceholder': 'Reserveringsnotities, bevestigingsnummer...',
'places.mapsSearchPlaceholder': 'Plaatsen zoeken...',
'places.mapsSearchError': 'Zoeken naar plaatsen mislukt.',
+ 'places.loadingDetails': 'Plaatsgegevens laden…',
'places.osmHint': 'Zoeken via OpenStreetMap (geen foto\'s, openingstijden of beoordelingen). Voeg een Google API-sleutel toe in instellingen voor volledige details.',
'places.osmActive': 'Zoeken via OpenStreetMap (geen foto\'s, beoordelingen of openingstijden). Voeg een Google API-sleutel toe in Instellingen voor uitgebreide gegevens.',
'places.categoryCreateError': 'Categorie aanmaken mislukt',
diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts
index 4f60b983..5870f5c9 100644
--- a/client/src/i18n/translations/pl.ts
+++ b/client/src/i18n/translations/pl.ts
@@ -811,6 +811,7 @@ const pl: Record = {
'places.reservationNotesPlaceholder': 'Notatki z rezerwacji, numer potwierdzenia...',
'places.mapsSearchPlaceholder': 'Szukaj miejsc...',
'places.mapsSearchError': 'Nie udało się wyszukać miejsca.',
+ 'places.loadingDetails': 'Ładowanie szczegółów miejsca…',
'places.osmHint': 'Korzystając z OpenStreetMap (brak zdjęć, godzin otwarcia czy ocen). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.',
'places.osmActive': 'Szukaj przez OpenStreetMap (brak zdjęć, ocen czy godzin otwarcia). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.',
'places.categoryCreateError': 'Nie udało się utworzyć kategorii',
diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts
index 18001fc9..a9624ad7 100644
--- a/client/src/i18n/translations/ru.ts
+++ b/client/src/i18n/translations/ru.ts
@@ -852,6 +852,7 @@ const ru: Record = {
'places.reservationNotesPlaceholder': 'Заметки о бронировании, номер подтверждения...',
'places.mapsSearchPlaceholder': 'Поиск мест...',
'places.mapsSearchError': 'Ошибка поиска мест.',
+ 'places.loadingDetails': 'Загрузка данных о месте…',
'places.osmHint': 'Поиск через OpenStreetMap (без фото, часов работы и рейтингов). Добавьте API-ключ Google в настройках для полной информации.',
'places.osmActive': 'Поиск через OpenStreetMap (без фото, рейтингов и часов работы). Добавьте API-ключ Google в настройках для расширенных данных.',
'places.categoryCreateError': 'Не удалось создать категорию',
diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts
index d0af81d3..11326296 100644
--- a/client/src/i18n/translations/zh.ts
+++ b/client/src/i18n/translations/zh.ts
@@ -852,6 +852,7 @@ const zh: Record = {
'places.reservationNotesPlaceholder': '预订备注、确认号...',
'places.mapsSearchPlaceholder': '搜索地点...',
'places.mapsSearchError': '地点搜索失败。',
+ 'places.loadingDetails': '正在加载地点详情…',
'places.osmHint': '使用 OpenStreetMap 搜索(无照片、营业时间或评分)。在设置中添加 Google API 密钥以获取完整信息。',
'places.osmActive': '通过 OpenStreetMap 搜索(无照片、评分或营业时间)。在设置中添加 Google API 密钥以获取增强数据。',
'places.categoryCreateError': '创建分类失败',
diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts
index 86fa1418..9f270f58 100644
--- a/client/src/i18n/translations/zhTw.ts
+++ b/client/src/i18n/translations/zhTw.ts
@@ -832,6 +832,7 @@ const zhTw: Record = {
'places.reservationNotesPlaceholder': '預訂備註、確認號...',
'places.mapsSearchPlaceholder': '搜尋地點...',
'places.mapsSearchError': '地點搜尋失敗。',
+ 'places.loadingDetails': '正在載入地點詳情…',
'places.osmHint': '使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。',
'places.osmActive': '透過 OpenStreetMap 搜尋(無照片、評分或營業時間)。在設定中新增 Google API 金鑰以獲取增強資料。',
'places.categoryCreateError': '建立分類失敗',
diff --git a/client/src/store/authStore.ts b/client/src/store/authStore.ts
index d7a11de1..2fb289ad 100644
--- a/client/src/store/authStore.ts
+++ b/client/src/store/authStore.ts
@@ -178,6 +178,7 @@ export const useAuthStore = create((set, get) => ({
await authApi.updateMapsKey(key)
set((state) => ({
user: state.user ? { ...state.user, maps_api_key: key || null } : null,
+ hasMapsKey: !!key,
}))
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error saving API key'))
@@ -188,6 +189,9 @@ export const useAuthStore = create((set, get) => ({
try {
const data = await authApi.updateApiKeys(keys)
set({ user: data.user })
+ if ('maps_api_key' in keys) {
+ set({ hasMapsKey: !!keys.maps_api_key })
+ }
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error saving API keys'))
}
diff --git a/server/src/services/mapsService.ts b/server/src/services/mapsService.ts
index 119089cb..2f537850 100644
--- a/server/src/services/mapsService.ts
+++ b/server/src/services/mapsService.ts
@@ -115,6 +115,34 @@ export async function searchNominatim(query: string, lang?: string) {
}));
}
+// ── Nominatim lookup (by OSM ID) ────────────────────────────────────────────
+
+export async function lookupNominatim(osmType: string, osmId: string, lang?: string): Promise<{
+ name: string; address: string; lat: number | null; lng: number | null;
+} | null> {
+ const typePrefix = osmType.charAt(0).toUpperCase(); // N, W, R
+ const params = new URLSearchParams({
+ osm_ids: `${typePrefix}${osmId}`,
+ format: 'json',
+ 'accept-language': lang || 'en',
+ });
+ try {
+ const res = await fetch(`https://nominatim.openstreetmap.org/lookup?${params}`, {
+ headers: { 'User-Agent': UA },
+ });
+ if (!res.ok) return null;
+ const data = await res.json() as NominatimResult[];
+ const item = data[0];
+ if (!item) return null;
+ return {
+ name: item.name || item.display_name?.split(',')[0] || '',
+ address: item.display_name || '',
+ lat: parseFloat(item.lat) || null,
+ lng: parseFloat(item.lon) || null,
+ };
+ } catch { return null; }
+}
+
// ── Overpass API (OSM details) ───────────────────────────────────────────────
export async function fetchOverpassDetails(osmType: string, osmId: string): Promise {
@@ -396,9 +424,21 @@ export async function getPlaceDetails(userId: number, placeId: string, lang?: st
// OSM details: placeId is "node:123456" or "way:123456" etc.
if (placeId.includes(':')) {
const [osmType, osmId] = placeId.split(':');
- const element = await fetchOverpassDetails(osmType, osmId);
- if (!element?.tags) return { place: buildOsmDetails({}, osmType, osmId) };
- return { place: buildOsmDetails(element.tags, osmType, osmId) };
+ const [element, nominatim] = await Promise.all([
+ fetchOverpassDetails(osmType, osmId),
+ lookupNominatim(osmType, osmId, lang),
+ ]);
+ const details = buildOsmDetails(element?.tags || {}, osmType, osmId);
+ return {
+ place: {
+ ...details,
+ name: nominatim?.name || element?.tags?.name || '',
+ address: nominatim?.address || '',
+ lat: nominatim?.lat ?? null,
+ lng: nominatim?.lng ?? null,
+ osm_id: placeId,
+ },
+ };
}
// Google details