"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var GeocodingService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeocodingService = void 0; const common_1 = require("@nestjs/common"); const typeorm_1 = require("@nestjs/typeorm"); const typeorm_2 = require("typeorm"); const cache_manager_1 = require("@nestjs/cache-manager"); const place_syria_entity_1 = require("./entities/place-syria.entity"); const place_jordan_entity_1 = require("./entities/place-jordan.entity"); const place_egypt_entity_1 = require("./entities/place-egypt.entity"); const osm_area_entity_1 = require("./entities/osm-area.entity"); const osm_point_with_area_entity_1 = require("./entities/osm-point-with-area.entity"); let GeocodingService = GeocodingService_1 = class GeocodingService { placesSyriaRepository; placesJordanRepository; placesEgyptRepository; osmAreasRepository; osmPointsRepository; cacheManager; logger = new common_1.Logger(GeocodingService_1.name); DB_TIMEOUT_MS = 1100; constructor(placesSyriaRepository, placesJordanRepository, placesEgyptRepository, osmAreasRepository, osmPointsRepository, cacheManager) { this.placesSyriaRepository = placesSyriaRepository; this.placesJordanRepository = placesJordanRepository; this.placesEgyptRepository = placesEgyptRepository; this.osmAreasRepository = osmAreasRepository; this.osmPointsRepository = osmPointsRepository; this.cacheManager = cacheManager; } getRepositoryForCoords(lat, lng) { if (lat >= 29 && lat <= 37.5 && lng >= 34.5 && lng <= 42.5) { if (lat > 32.5 && lng > 35.8) return this.placesSyriaRepository; return this.placesJordanRepository; } if (lat >= 22 && lat <= 32 && lng >= 24.5 && lng <= 37) { return this.placesEgyptRepository; } return this.placesSyriaRepository; } identifyRegion(lat, lng) { if (lat === undefined || lng === undefined) return undefined; if (lat >= 29 && lat <= 37.5 && lng >= 34.5 && lng <= 42.5) { if (lat > 32.5 && lng > 35.8) return 'syria'; return 'jordan'; } if (lat >= 22 && lat <= 32 && lng >= 24.5 && lng <= 37) { return 'egypt'; } return undefined; } getTableNameForRepo(repo) { if (repo === this.placesJordanRepository) return 'places_jordan'; if (repo === this.placesEgyptRepository) return 'places_egypt'; return 'places_syria'; } async searchPlaces(query, lat, lon, radius = 20000, country) { try { const cleanQuery = query.trim(); if (!cleanQuery) return { results: [] }; const hasLocation = lat !== undefined && lon !== undefined; const geoSegment = hasLocation ? `${lat.toFixed(2)}_${lon.toFixed(2)}` : 'global'; const cacheKey = `geo_search:${country || 'auto'}:${geoSegment}:${cleanQuery.toLowerCase()}`; const cached = await this.cacheManager.get(cacheKey); if (cached) return { results: cached, source: 'cache_hit' }; let targetRegion = country?.toLowerCase() || this.identifyRegion(lat, lon); const primaryTables = targetRegion && ['syria', 'egypt', 'jordan'].includes(targetRegion) ? [`places_${targetRegion}`] : ['places_jordan', 'places_syria', 'places_egypt']; const queryPromises = []; primaryTables.forEach(tableName => { const repo = this.getRepoByTableName(tableName); queryPromises.push(repo.query(` SELECT p.id, p.name, p.name_ar, p.name_en, p.category, n.name_ar as neighbourhood, d.name_ar as district, g.name_ar as governorate, p.latitude, p.longitude, p.address, '${tableName.replace('places_', '')}' as region, 'user_place' as source, CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(p.location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance, GREATEST(similarity(COALESCE(p.name_ar, ''), $1), similarity(COALESCE(p.name, ''), $1)) as relevance FROM ${tableName} p LEFT JOIN neighborhood_polygons n ON p.neighborhood_id = n.id LEFT JOIN admin_boundaries d ON p.district_id = d.id LEFT JOIN admin_boundaries g ON p.governorate_id = g.id WHERE (p.name_ar % $1 OR p.name % $1) AND ($2::float IS NULL OR ST_DistanceSphere(p.location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $4) ORDER BY (p.name_ar <-> $1) ASC LIMIT 10 `, [cleanQuery, lat || null, lon || null, radius])); }); queryPromises.push(this.osmPointsRepository.query(` SELECT o.osm_id as id, o.name, o.name_ar, o.name_en, COALESCE(o.amenity, o.shop, 'place') as category, o.latitude, o.longitude, o.addr_street as address, 'osm_global' as source, CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(o.geom, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance, GREATEST(similarity(COALESCE(o.name, ''), $1), similarity(COALESCE(o.name_ar, ''), $1)) as relevance FROM osm_points_with_area o WHERE (o.name % $1 OR o.name_ar % $1) AND ($2::float IS NULL OR ST_DistanceSphere(o.geom, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $4) ORDER BY (o.name <-> $1) ASC LIMIT 10 `, [cleanQuery, lat || null, lon || null, radius])); const executionResults = await Promise.race([ Promise.allSettled(queryPromises), new Promise((_, reject) => setTimeout(() => reject(new Error('QUERY_TIMEOUT')), this.DB_TIMEOUT_MS)) ]).catch(e => { this.logger.warn(`Search optimization threshold hit: ${e.message}`); return []; }); let allResults = []; if (Array.isArray(executionResults)) { executionResults.forEach(res => { if (res.status === 'fulfilled' && res.value) allResults.push(...res.value); }); } const formatted = this.formatResults(allResults, hasLocation); if (formatted.length > 0) { await this.cacheManager.set(cacheKey, formatted, 3600000); } return { results: formatted }; } catch (e) { this.logger.error('Search failed:', e); return { results: [] }; } } formatResults(results, hasLocation) { const seenStreets = new Set(); return results .map(r => { const proximityBonus = hasLocation ? Math.max(0, 1 - (Number(r.distance) / 10000)) : 0; const totalScore = (Number(r.relevance) * 0.7) + (proximityBonus * 0.3); return { ...r, totalScore }; }) .sort((a, b) => b.totalScore - a.totalScore) .filter(r => { if (r.category === 'street') { const key = `${r.name_ar || r.name}_${r.district}_${r.governorate}`; if (seenStreets.has(key)) return false; seenStreets.add(key); } return true; }) .slice(0, 4) .map(r => { const addressParts = [r.neighbourhood, r.district, r.governorate].filter(Boolean); const full_address = addressParts.length > 0 ? addressParts.join('، ') : (r.address || ''); return { ...r, latitude: parseFloat(r.latitude), longitude: parseFloat(r.longitude), distance_km: r.distance ? (Number(r.distance) / 1000).toFixed(2) : null, location: { lat: parseFloat(r.latitude), lng: parseFloat(r.longitude) }, full_address, }; }); } getRepoByTableName(tableName) { if (tableName === 'places_egypt') return this.placesEgyptRepository; if (tableName === 'places_jordan') return this.placesJordanRepository; return this.placesSyriaRepository; } async reverseGeocode(lat, lng) { try { const repo = this.getRepositoryForCoords(lat, lng); const tableName = this.getTableNameForRepo(repo); const query = ` SELECT p.id, p.name, p.name_ar, p.category, n.name_ar as neighbourhood, d.name_ar as district, g.name_ar as governorate, p.latitude, p.longitude, p.address, 'user_place' as source, ST_DistanceSphere(p.location::geometry, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance FROM ${tableName} p LEFT JOIN neighborhood_polygons n ON p.neighborhood_id = n.id LEFT JOIN admin_boundaries d ON p.sub_district_id = d.id LEFT JOIN admin_boundaries g ON p.governorate_id = g.id WHERE p.location IS NOT NULL ORDER BY p.location::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 3 `; return await repo.query(query, [lng, lat]); } catch (error) { this.logger.error('Reverse geocoding error:', error); return []; } } async addPlace(data) { try { const lat = Number(data.latitude), lng = Number(data.longitude); const repo = this.getRepositoryForCoords(lat, lng), tableName = this.getTableNameForRepo(repo); const newPlace = repo.create({ ...data, latitude: lat, longitude: lng, created_at: new Date(), location: { type: 'Point', coordinates: [lng, lat] } }); const savedPlace = await repo.save(newPlace); await repo.query(`UPDATE ${tableName} SET location = ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) WHERE id = $3`, [lng, lat, savedPlace.id]); return { ...savedPlace, latitude: lat, longitude: lng, location: `POINT(${lng} ${lat})` }; } catch (error) { throw new common_1.HttpException(error.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR); } } async upsertPlace(data) { try { const lat = Number(data.latitude), lng = Number(data.longitude); const repo = this.getRepositoryForCoords(lat, lng), tableName = this.getTableNameForRepo(repo); const existing = await repo.query(`SELECT id, name FROM ${tableName} WHERE ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) < 15 LIMIT 1`, [lng, lat]); if (existing && existing.length > 0) { await repo.update(existing[0].id, { name: data.name || undefined, name_ar: data.name_ar || undefined, category: data.category || undefined }); return { id: existing[0].id, action: 'updated' }; } const result = await this.addPlace(data); return { id: result.id, action: 'created' }; } catch (error) { throw error; } } async upsertBatch(places) { const results = []; for (const place of places) { try { const r = await this.addPlace(place); results.push({ id: r.id, action: 'created' }); } catch (e) { } } return { total: places.length, processed: results.length, created: results.length, updated: 0 }; } async getRecentPlaces(limit = 50) { const s = await this.placesSyriaRepository.find({ order: { created_at: 'DESC' }, take: limit }); const j = await this.placesJordanRepository.find({ order: { created_at: 'DESC' }, take: limit }); const e = await this.placesEgyptRepository.find({ order: { created_at: 'DESC' }, take: limit }); return [...s, ...j, ...e].sort((a, b) => b.created_at.getTime() - a.created_at.getTime()).slice(0, limit); } async getAllPlacesGeoJSON() { try { const q = ` SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_syria UNION ALL SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_jordan UNION ALL SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_egypt UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Building') as name, 'building' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_building WHERE names->>'primary' IS NOT NULL LIMIT 500 UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Street') as name, 'street' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_segment WHERE names->>'primary' IS NOT NULL LIMIT 500 `; const res = await this.placesSyriaRepository.query(q); const features = res.map(p => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [parseFloat(p.longitude), parseFloat(p.latitude)] }, properties: { id: p.id, name: p.name, category: p.category, address: p.address, region: p.region } })); return { type: 'FeatureCollection', features }; } catch (e) { return { type: 'FeatureCollection', features: [] }; } } async deletePlacesByName(name, country) { const repo = this.getRepoByCountry(country); const r1 = await repo.delete({ name_ar: name }), r2 = await repo.delete({ name_en: name }), r3 = await repo.delete({ name: name }); return { success: true, affected: (r1.affected || 0) + (r2.affected || 0) + (r3.affected || 0) }; } async deletePlaceById(id, country) { const repo = this.getRepoByCountry(country); return { success: true, affected: (await repo.delete(id)).affected }; } getRepoByCountry(country) { switch (country?.toLowerCase()) { case 'syria': return this.placesSyriaRepository; case 'jordan': return this.placesJordanRepository; case 'egypt': return this.placesEgyptRepository; default: throw new common_1.HttpException('Invalid country: ' + country, common_1.HttpStatus.BAD_REQUEST); } } }; exports.GeocodingService = GeocodingService; exports.GeocodingService = GeocodingService = GeocodingService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, typeorm_1.InjectRepository)(place_syria_entity_1.PlaceSyria)), __param(1, (0, typeorm_1.InjectRepository)(place_jordan_entity_1.PlaceJordan)), __param(2, (0, typeorm_1.InjectRepository)(place_egypt_entity_1.PlaceEgypt)), __param(3, (0, typeorm_1.InjectRepository)(osm_area_entity_1.OsmArea)), __param(4, (0, typeorm_1.InjectRepository)(osm_point_with_area_entity_1.OsmPointWithArea)), __param(5, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)), __metadata("design:paramtypes", [typeorm_2.Repository, typeorm_2.Repository, typeorm_2.Repository, typeorm_2.Repository, typeorm_2.Repository, Object]) ], GeocodingService); //# sourceMappingURL=geocoding.service.js.map