2026-04-14-8 auth and commercial
This commit is contained in:
+257
-39
@@ -17,52 +17,188 @@ 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 {
|
||||
placesRepository;
|
||||
placesSyriaRepository;
|
||||
placesJordanRepository;
|
||||
placesEgyptRepository;
|
||||
osmAreasRepository;
|
||||
osmPointsRepository;
|
||||
cacheManager;
|
||||
logger = new common_1.Logger(GeocodingService_1.name);
|
||||
constructor(placesRepository, osmAreasRepository, osmPointsRepository) {
|
||||
this.placesRepository = placesRepository;
|
||||
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;
|
||||
}
|
||||
async searchPlaces(query) {
|
||||
if (!query || query.length < 3)
|
||||
return { userPlaces: [], osmAreas: [] };
|
||||
const userPlaces = await this.placesRepository.find({
|
||||
where: [
|
||||
{ name: (0, typeorm_2.Like)(`%${query}%`) },
|
||||
{ name_ar: (0, typeorm_2.Like)(`%${query}%`) },
|
||||
],
|
||||
take: 10,
|
||||
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,
|
||||
};
|
||||
});
|
||||
const osmAreas = await this.osmAreasRepository.find({
|
||||
where: [
|
||||
{ name: (0, typeorm_2.Like)(`%${query}%`) },
|
||||
{ name_ar: (0, typeorm_2.Like)(`%${query}%`) },
|
||||
],
|
||||
take: 10,
|
||||
});
|
||||
return {
|
||||
userPlaces,
|
||||
osmAreas,
|
||||
};
|
||||
}
|
||||
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 *, ST_Distance_Sphere(location, POINT(?, ?)) as distance
|
||||
FROM places_syria
|
||||
WHERE location IS NOT NULL
|
||||
ORDER BY distance ASC
|
||||
LIMIT 5
|
||||
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
|
||||
`;
|
||||
const results = await this.placesRepository.query(query, [lng, lat]);
|
||||
return results;
|
||||
return await repo.query(query, [lng, lat]);
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.error('Reverse geocoding error:', error);
|
||||
@@ -70,21 +206,103 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
|
||||
}
|
||||
}
|
||||
async addPlace(data) {
|
||||
const newPlace = this.placesRepository.create({
|
||||
...data,
|
||||
created_at: new Date(),
|
||||
});
|
||||
return this.placesRepository.save(newPlace);
|
||||
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, 'mysqlConnection')),
|
||||
__param(1, (0, typeorm_1.InjectRepository)(osm_area_entity_1.OsmArea, 'mysqlConnection')),
|
||||
__param(2, (0, typeorm_1.InjectRepository)(osm_point_with_area_entity_1.OsmPointWithArea, 'mysqlConnection')),
|
||||
__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,
|
||||
typeorm_2.Repository, Object])
|
||||
], GeocodingService);
|
||||
//# sourceMappingURL=geocoding.service.js.map
|
||||
Reference in New Issue
Block a user