const https = require('https'); const fs = require('fs').promises; const path = require('path'); const config = require('../config/env'); const logger = require('./logger'); const CACHE_FILE = path.join(__dirname, '..', 'geocode-cache.json'); const CACHE_SAVE_INTERVAL = 60000; // Save every 60 seconds const cache = new Map(); let lastRequestTime = 0; let cacheModified = false; // Load cache from file on startup const loadCache = async () => { try { const data = await fs.readFile(CACHE_FILE, 'utf8'); const parsed = JSON.parse(data); Object.entries(parsed).forEach(([key, value]) => { cache.set(key, value); }); logger.info(`Geocoding cache loaded: ${cache.size} entries`); } catch (error) { if (error.code !== 'ENOENT') { logger.warn('Failed to load geocoding cache:', error.message); } } }; // Save cache to file const saveCache = async () => { if (!cacheModified) return; try { const obj = Object.fromEntries(cache); await fs.writeFile(CACHE_FILE, JSON.stringify(obj, null, 2)); cacheModified = false; logger.info(`Geocoding cache saved: ${cache.size} entries`); } catch (error) { logger.error('Failed to save geocoding cache:', error.message); } }; // Periodically save cache setInterval(saveCache, CACHE_SAVE_INTERVAL); // Save on process exit process.on('SIGINT', async () => { await saveCache(); process.exit(0); }); process.on('SIGTERM', async () => { await saveCache(); process.exit(0); }); // Initialize cache loading loadCache().catch(err => logger.error('Cache initialization error:', err)); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const fetchJson = (url, headers) => new Promise((resolve, reject) => { const req = https.get(url, { headers }, (res) => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { if (res.statusCode && res.statusCode >= 400) { return reject(new Error(`Geocode HTTP ${res.statusCode}`)); } try { resolve(JSON.parse(data)); } catch (error) { reject(error); } }); }); req.on('error', reject); req.end(); }); const geocodeAddress = async (address) => { const normalized = (address || '').trim(); if (!normalized) { return null; } const cacheKey = normalized.toLowerCase(); if (cache.has(cacheKey)) { return cache.get(cacheKey); } const elapsed = Date.now() - lastRequestTime; if (elapsed < config.geocodeMinDelayMs) { await sleep(config.geocodeMinDelayMs - elapsed); } const url = `${config.geocodeUrl}?format=json&limit=1&countrycodes=de&q=${encodeURIComponent(normalized)}`; const headers = { 'User-Agent': config.geocodeUserAgent }; try { const results = await fetchJson(url, headers); lastRequestTime = Date.now(); if (!Array.isArray(results) || results.length === 0) { cache.set(cacheKey, null); cacheModified = true; return null; } const hit = results[0]; const lat = parseFloat(hit.lat); const lng = parseFloat(hit.lon); if (Number.isNaN(lat) || Number.isNaN(lng)) { cache.set(cacheKey, null); cacheModified = true; return null; } const coords = { lat, lng }; cache.set(cacheKey, coords); cacheModified = true; return coords; } catch (error) { logger.warn('Geocoding fehlgeschlagen', { address: normalized, error: error.message }); lastRequestTime = Date.now(); return null; } }; module.exports = { geocodeAddress };