Files
flyer-crawler.projectium.com/src/services/geocodingService.server.ts
2025-12-22 13:22:21 -08:00

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,
);