114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import type { Logger } from 'pino';
|
|
import { connection as redis } from './queueService.server';
|
|
import { googleGeocodingService, GoogleGeocodingService } from './googleGeocodingService.server';
|
|
import {
|
|
nominatimGeocodingService,
|
|
NominatimGeocodingService,
|
|
} from './nominatimGeocodingService.server';
|
|
|
|
export class GeocodingService {
|
|
constructor(
|
|
private googleService: GoogleGeocodingService,
|
|
private nominatimService: NominatimGeocodingService,
|
|
) {}
|
|
|
|
async geocodeAddress(
|
|
address: string,
|
|
logger: Logger,
|
|
): Promise<{ lat: number; lng: number } | null> {
|
|
const cacheKey = `geocode:${address}`;
|
|
|
|
try {
|
|
const cached = await redis.get(cacheKey);
|
|
if (cached) {
|
|
logger.info({ cacheKey }, 'Geocoding result found in cache.');
|
|
return JSON.parse(cached);
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
{ err: error instanceof Error ? error.message : error, cacheKey },
|
|
'Redis GET or JSON.parse command failed. Proceeding without cache.',
|
|
);
|
|
}
|
|
|
|
if (process.env.GOOGLE_MAPS_API_KEY) {
|
|
try {
|
|
const coordinates = await this.googleService.geocode(address, logger);
|
|
if (coordinates) {
|
|
await this.setCache(cacheKey, coordinates, logger);
|
|
return coordinates;
|
|
}
|
|
logger.info(
|
|
{ address, provider: 'Google' },
|
|
'Google Geocoding returned no results. Falling back to Nominatim.',
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
{ err: error instanceof Error ? error.message : error },
|
|
'An error occurred while calling the Google Maps Geocoding API. Falling back to Nominatim.',
|
|
);
|
|
}
|
|
} else {
|
|
logger.warn(
|
|
'GOOGLE_MAPS_API_KEY is not set. Falling back to Nominatim as the primary geocoding provider.',
|
|
);
|
|
}
|
|
|
|
const nominatimResult = await this.nominatimService.geocode(address, logger);
|
|
if (nominatimResult) {
|
|
await this.setCache(cacheKey, nominatimResult, logger);
|
|
return nominatimResult;
|
|
}
|
|
|
|
logger.error({ address }, 'All geocoding providers failed.');
|
|
return null;
|
|
}
|
|
|
|
private async setCache(
|
|
cacheKey: string,
|
|
result: { lat: number; lng: number },
|
|
logger: Logger,
|
|
): Promise<void> {
|
|
try {
|
|
await redis.set(cacheKey, JSON.stringify(result), 'EX', 60 * 60 * 24 * 30); // Cache for 30 days
|
|
} catch (error) {
|
|
logger.error(
|
|
{ err: error instanceof Error ? error.message : error, cacheKey },
|
|
'Redis SET command failed. Result will not be cached.',
|
|
);
|
|
}
|
|
}
|
|
|
|
async clearGeocodeCache(logger: Logger): Promise<number> {
|
|
let cursor = '0';
|
|
let totalDeleted = 0;
|
|
const pattern = 'geocode:*';
|
|
logger.info(`Starting geocode cache clear with pattern: ${pattern}`);
|
|
|
|
try {
|
|
do {
|
|
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
|
cursor = nextCursor;
|
|
if (keys.length > 0) {
|
|
const deletedCount = await redis.del(keys);
|
|
totalDeleted += deletedCount;
|
|
}
|
|
} while (cursor !== '0');
|
|
|
|
logger.info(`Successfully deleted ${totalDeleted} geocode cache entries.`);
|
|
return totalDeleted;
|
|
} catch (error) {
|
|
logger.error(
|
|
{ err: error instanceof Error ? error.message : error },
|
|
'Failed to clear geocode cache from Redis.',
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const geocodingService = new GeocodingService(
|
|
googleGeocodingService,
|
|
nominatimGeocodingService,
|
|
);
|