143 lines
3.6 KiB
JavaScript
143 lines
3.6 KiB
JavaScript
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).unref();
|
|
|
|
// 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 };
|
|
if (cache.size >= 1000) { cache.delete(cache.keys().next().value); }
|
|
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
|
|
};
|