const express = require('express'); const router = express.Router(); const rateLimit = require('express-rate-limit'); const { reverseGeocode, forwardGeocode, forwardGeocodeSearch, getCacheStats } = require('../services/geocoding'); // Rate limiter specifically for geocoding endpoints const geocodeLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 30, // limit each IP to 30 requests per windowMs trustProxy: true, // Explicitly trust proxy keyGenerator: (req) => { return req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for']?.split(',')[0] || req.ip; }, message: 'Too many geocoding requests, please try again later.' }); /** * Reverse geocode endpoint * GET /api/geocode/reverse?lat=&lng= */ router.get('/reverse', geocodeLimiter, async (req, res) => { try { const { lat, lng } = req.query; // Validate input if (!lat || !lng) { return res.status(400).json({ success: false, error: 'Latitude and longitude are required' }); } const latitude = parseFloat(lat); const longitude = parseFloat(lng); if (isNaN(latitude) || isNaN(longitude)) { return res.status(400).json({ success: false, error: 'Invalid latitude or longitude' }); } if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) { return res.status(400).json({ success: false, error: 'Coordinates out of range' }); } // Perform reverse geocoding const result = await reverseGeocode(latitude, longitude); res.json({ success: true, data: result }); } catch (error) { console.error('Reverse geocoding error:', error); const statusCode = error.message.includes('Rate limit') ? 429 : 500; res.status(statusCode).json({ success: false, error: error.message }); } }); /** * Forward geocode endpoint * GET /api/geocode/forward?address=
*/ router.get('/forward', geocodeLimiter, async (req, res) => { try { const { address } = req.query; // Validate input if (!address || address.trim().length === 0) { return res.status(400).json({ success: false, error: 'Address is required' }); } // Perform forward geocoding const result = await forwardGeocode(address); res.json({ success: true, data: result }); } catch (error) { console.error('Forward geocoding error:', error); const statusCode = error.message.includes('Rate limit') ? 429 : 500; res.status(statusCode).json({ success: false, error: error.message }); } }); /** * Forward geocode search endpoint (returns multiple results) * GET /api/geocode/search?query=
&limit= */ router.get('/search', geocodeLimiter, async (req, res) => { try { const { query, limit } = req.query; // Validate input if (!query || query.trim().length === 0) { return res.status(400).json({ success: false, error: 'Search query is required' }); } // Minimum search length if (query.trim().length < 2) { return res.json({ success: true, data: [] }); } const searchLimit = parseInt(limit) || 5; if (searchLimit < 1 || searchLimit > 10) { return res.status(400).json({ success: false, error: 'Limit must be between 1 and 10' }); } // Perform forward geocoding search const results = await forwardGeocodeSearch(query, searchLimit); res.json({ success: true, data: results, query: query, count: results.length }); } catch (error) { console.error('Forward geocoding search error:', error); const statusCode = error.message.includes('Rate limit') ? 429 : 500; res.status(statusCode).json({ success: false, error: error.message }); } }); /** * Get geocoding cache statistics (admin endpoint) * GET /api/geocode/cache/stats */ router.get('/cache/stats', (req, res) => { res.json({ success: true, data: getCacheStats() }); }); module.exports = router;