UNPKG

@siva-sub/mcp-public-transport

Version:

A Model Context Protocol server for Singapore transport data with real-time information and routing

196 lines (195 loc) 7.9 kB
import { logger } from '../utils/logger.js'; import { APIError, ValidationError } from '../utils/errors.js'; export class PostalCodeService { oneMapService; cache; constructor(oneMapService, cache) { this.oneMapService = oneMapService; this.cache = cache; } isValidSingaporePostalCode(code) { // Singapore postal codes are exactly 6 digits return /^\d{6}$/.test(code); } extractPostalCodeFromText(text) { // Extract 6-digit postal codes from text const matches = text.match(/\b\d{6}\b/g); return matches ? matches[0] : null; } async getLocationFromPostalCode(postalCode) { if (!this.isValidSingaporePostalCode(postalCode)) { throw new ValidationError(`Invalid Singapore postal code: ${postalCode}`); } const cacheKey = `postal_${postalCode}`; if (this.cache) { try { const cached = await this.cache.get(cacheKey); if (cached) { logger.debug(`Postal code ${postalCode} found in cache`); return cached; } } catch (error) { logger.warn('Cache lookup failed for postal code', { postalCode, error }); } } try { logger.info(`Resolving postal code: ${postalCode}`); // Search using postal code directly const searchResults = await this.oneMapService.geocodeWithAuth(postalCode); if (searchResults.length === 0) { logger.warn(`No results found for postal code: ${postalCode}`); return null; } // Take the first result (most relevant) const result = this.convertToLocationResult(searchResults[0], postalCode); // Cache successful results for 24 hours if (this.cache) { try { await this.cache.set(cacheKey, result, 86400); logger.debug(`Cached postal code result: ${postalCode}`); } catch (error) { logger.warn('Failed to cache postal code result', { postalCode, error }); } } logger.info(`Successfully resolved postal code ${postalCode} to ${result.displayName}`); return result; } catch (error) { logger.error(`Failed to resolve postal code ${postalCode}`, error); if (error instanceof APIError) { throw error; } throw new APIError(`Failed to resolve postal code ${postalCode}`, 'POSTAL_CODE_RESOLUTION_FAILED', 500); } } async validateAndResolvePostalCode(input) { // First try to extract postal code from input const postalCode = this.extractPostalCodeFromText(input); if (!postalCode) { // If input is exactly 6 digits, treat as postal code if (this.isValidSingaporePostalCode(input.trim())) { return this.getLocationFromPostalCode(input.trim()); } return null; } return this.getLocationFromPostalCode(postalCode); } convertToLocationResult(oneMapResult, postalCode) { const lat = parseFloat(oneMapResult.LATITUDE); const lng = parseFloat(oneMapResult.LONGITUDE); const x = parseFloat(oneMapResult.X); const y = parseFloat(oneMapResult.Y); // Validate coordinates if (isNaN(lat) || isNaN(lng) || isNaN(x) || isNaN(y)) { throw new APIError(`Invalid coordinates in OneMap response for postal code ${postalCode}`, 'INVALID_COORDINATES', 422); } // Determine building name (handle 'NIL' values from OneMap) const buildingName = oneMapResult.BUILDING && oneMapResult.BUILDING !== 'NIL' ? oneMapResult.BUILDING : undefined; const blockNumber = oneMapResult.BLK_NO && oneMapResult.BLK_NO !== 'NIL' ? oneMapResult.BLK_NO : undefined; // Create a meaningful display name let displayName = oneMapResult.ADDRESS; if (!displayName || displayName === 'NIL') { displayName = buildingName ? `${buildingName}, Singapore ${postalCode}` : `${oneMapResult.ROAD_NAME}, Singapore ${postalCode}`; } // Determine location name const name = buildingName || oneMapResult.ROAD_NAME || `Postal Code ${postalCode}`; return { id: `postal_${postalCode}`, name, displayName, coordinates: { wgs84: { lat, lng }, svy21: { x, y }, }, address: { buildingName, blockNumber, streetName: oneMapResult.ROAD_NAME, postalCode: oneMapResult.POSTAL || postalCode, formattedAddress: oneMapResult.ADDRESS, }, locationTypes: ['POSTAL_CODE'], confidence: 0.95, // High confidence for exact postal code matches metadata: { source: 'onemap', lastUpdated: new Date().toISOString(), isLandmark: false, searchStrategy: 'postal_code', }, }; } // Get district information from postal code getDistrictFromPostalCode(postalCode) { if (!this.isValidSingaporePostalCode(postalCode)) { return null; } const firstTwoDigits = parseInt(postalCode.substring(0, 2)); // Singapore postal code district mapping (simplified) const districtMap = { '01-09': 'Downtown Core / Boat Quay / Chinatown', '10-16': 'Tanjong Pagar / Shenton Way', '17-19': 'Newton / Novena', '20-21': 'Orchard / River Valley', '22-23': 'Bukit Timah', '24-27': 'Clementi / West Coast', '28-30': 'Jurong', '31-33': 'Bukit Batok / Choa Chu Kang', '34-36': 'Kranji / Woodlands', '37-38': 'Yishun', '39-41': 'Ang Mo Kio', '42-45': 'Bishan / Thomson', '46-48': 'Toa Payoh', '49-50': 'Balestier / Serangoon', '51-52': 'MacPherson / Potong Pasir', '53-55': 'Geylang', '56-57': 'Katong / Marine Parade', '58-59': 'Bedok', '60-64': 'Changi / Tampines', '65-67': 'Pasir Ris', '68-69': 'Sengkang', '70-73': 'Hougang / Punggol', '75-80': 'Yio Chu Kang / Seletar', '81': 'Sentosa', '82': 'Punggol', }; for (const [range, district] of Object.entries(districtMap)) { const [start, end] = range.split('-').map(num => parseInt(num)); if (firstTwoDigits >= start && firstTwoDigits <= end) { return district; } } return null; } // Check if postal code is in a specific area isInArea(postalCode, area) { const district = this.getDistrictFromPostalCode(postalCode); return district ? district.toLowerCase().includes(area.toLowerCase()) : false; } // Get area type from postal code getAreaType(postalCode) { if (!this.isValidSingaporePostalCode(postalCode)) { return null; } const firstTwoDigits = parseInt(postalCode.substring(0, 2)); if (firstTwoDigits >= 1 && firstTwoDigits <= 30) { return 'Central/West Singapore'; } else if (firstTwoDigits >= 31 && firstTwoDigits <= 50) { return 'North Singapore'; } else if (firstTwoDigits >= 51 && firstTwoDigits <= 82) { return 'East Singapore'; } return null; } }