feat: add place gate population scripts, map style assets, automated cron sync tasks, and expand dashboard and landing applications

This commit is contained in:
Hamza-Ayed
2026-09-19 21:22:05 +03:00
parent 82b1045f9c
commit ce31d79ba6
76 changed files with 118969 additions and 115428 deletions
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# ==============================================================================
# SIRO Maps — Daily Database Backup Script
# ==============================================================================
set -euo pipefail
APP_DIR="${APP_DIR:-/home/hamzadoctor/app}"
BACKUP_DIR="${APP_DIR}/backups/db"
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
BACKUP_FILE="${BACKUP_DIR}/mapdb_custom_tables_${TIMESTAMP}.sql.gz"
RETENTION_DAYS=7
mkdir -p "$BACKUP_DIR"
echo "💾 [$(date '+%Y-%m-%d %H:%M:%S')] Starting SIRO Maps custom tables backup..."
# Dump custom tables: places_*, place_gates, billing, users, telemetry
docker compose -f "${APP_DIR}/docker-compose.yml" exec -T db pg_dump -U mapuser -d mapdb \
-t 'places_*' \
-t 'place_gates' \
-t 'users' \
-t 'api_keys' \
-t 'billing_*' \
-t 'invoices' \
-t 'subscriptions' \
-t 'pricing_plans' \
-t 'telemetry_*' \
--no-owner --no-privileges | gzip > "$BACKUP_FILE"
echo "✅ Backup created successfully: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
# Prune backups older than RETENTION_DAYS
echo "🧹 Pruning backups older than ${RETENTION_DAYS} days..."
find "$BACKUP_DIR" -type f -name "*.sql.gz" -mtime +"$RETENTION_DAYS" -delete
echo "🏁 [$(date '+%Y-%m-%d %H:%M:%S')] Backup process finished."
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# ==============================================================================
# SIRO Maps — Weekly Maintenance & Log Cleanup Script
# ==============================================================================
set -euo pipefail
APP_DIR="${APP_DIR:-/home/hamzadoctor/app}"
LOG_DIR="${APP_DIR}/logs"
RETENTION_DAYS=14
echo "🧹 [$(date '+%Y-%m-%d %H:%M:%S')] Starting weekly log and docker maintenance..."
# 1. Clean old log files
if [ -d "$LOG_DIR" ]; then
find "$LOG_DIR" -type f -name "*.log" -mtime +"$RETENTION_DAYS" -exec rm -f {} +
echo "✅ Removed log files older than ${RETENTION_DAYS} days."
fi
# 2. Truncate current active logs if they exceed 50MB
for log in "$LOG_DIR"/*.log; do
if [ -f "$log" ]; then
size=$(stat -c%s "$log" 2>/dev/null || stat -f%z "$log" 2>/dev/null || echo 0)
if [ "$size" -gt 52428800 ]; then # 50MB
tail -n 10000 "$log" > "${log}.tmp" && mv "${log}.tmp" "$log"
echo "✂️ Truncated large log file: $log"
fi
fi
done
# 3. Clean dangling docker images and build cache
docker image prune -f --filter "until=168h" > /dev/null 2>&1 || true
docker builder prune -f --keep-storage 2GB > /dev/null 2>&1 || true
echo "🏁 [$(date '+%Y-%m-%d %H:%M:%S')] Cleanup completed successfully."
+464
View File
@@ -0,0 +1,464 @@
#!/usr/bin/env python3
"""
Enrich Egypt Places Dataset (`places_egypt`) from:
1. OpenStreetMap Named Points (`planet_osm_point`)
2. OpenStreetMap Named Polygons (`planet_osm_polygon`)
3. Overture Maps Places (`overture_place`)
4. Deduplicate using spatial grid (<60m) against existing places_egypt
5. Refresh `unified_search_index`
"""
import os
import sys
import math
import time
import argparse
from collections import defaultdict
def haversine(lat1, lon1, lat2, lon2):
R = 6371000 # meters
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
delta_phi = math.radians(lat2 - lat1)
delta_lambda = math.radians(lon2 - lon1)
a = math.sin(delta_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2)**2
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def normalize_text(t):
if not t:
return ''
s = t.strip()
s = s.replace('أ', 'ا').replace('إ', 'ا').replace('آ', 'ا').replace('ة', 'ه').replace('ى', 'ي')
return ' '.join(s.split()).lower()
def clean_sql_str(val):
if val is None:
return 'NULL'
s = str(val).strip().replace("'", "''")
return f"'{s}'"
OSM_CAT_MAP = {
'village': 'قرية / تجمع سكاني',
'town': 'بلدة / مركز',
'city': 'مدينة',
'suburb': 'حي / ضاحية',
'neighbourhood': 'منطقة سكنية',
'hamlet': 'نجع / كفر',
'locality': 'موضع / منطقة',
'school': 'مدرسة / تعليم',
'college': 'كلية',
'university': 'جامعة',
'kindergarten': 'روضة أطفال',
'supermarket': 'سوبرماركت وهايبرماركت',
'convenience': 'محل بقالة',
'bakery': 'مخبز وفرن',
'butcher': 'جزارة وقصابة',
'mall': 'مركز تسوق ومول',
'place_of_worship': 'مسجد / دار عبادة',
'mosque': 'جامع / مسجد',
'clinic': 'مركز طبي وعيادة',
'pharmacy': 'صيدلية',
'hospital': 'مستشفى',
'doctors': 'عيادات تخصصية',
'dentist': 'عيادة أسنان',
'restaurant': 'مطعم',
'cafe': 'مقهى وكافيه',
'fast_food': 'مأكولات سريعة',
'fuel': 'محطة وقود وبنزين',
'bank': 'مصرف / بنك',
'atm': 'ماكينة صراف آلي ATM',
'bureau_de_change': 'مكتب صرافة',
'police': 'قسم شرطة ونقطة أمنية',
'fire_station': 'وحدة مطافئ / دفاع مدني',
'post_office': 'مكتب بريد',
'attraction': 'معلم سياحي وتاريخي',
'hotel': 'فندق وإقامة',
'guest_house': 'نُزل واستضافة',
'museum': 'متحف وآثار',
'park': 'حديقة ومنتزه عام',
'stadium': 'استاد وملعب رياضي',
'sports_centre': 'مركز ونادي رياضي',
'car_repair': 'ورشة صيانة سيارات',
'car_wash': 'مغسلة سيارات',
'company': 'شركة ومقر أعمال',
'government': 'جهة حكومية ورسمية',
'mobile_phone': 'متجر هواتف ومحمول',
'clothes': 'متجر ملابس وأزياء',
'shoes': 'متجر أحذية',
'hairdresser': 'صالون حلاقة وتجميل',
'optician': 'بصريات ونظارات',
'hardware': 'أدوات بناء وتجهيزات',
}
OVERTURE_CAT_MAP = {
'restaurant': 'مطعم',
'cafe': 'مقهى وكافيه',
'coffee_shop': 'مقهى وكوفي شوب',
'casual_eatery': 'مأكولات سريعة وخفيفة',
'bakery': 'مخبز ومعجنات',
'food_and_beverage_store': 'بقالة ومواد غذائية',
'supermarket': 'سوبرماركت وهايبرماركت',
'fashion_and_apparel_store': 'متجر ملابس وأزياء',
'shoe_store': 'متجر أحذية',
'jewelry_store': 'مجوهرات وحلي',
'electronics_store': 'متجر إلكترونيات وأجهزة',
'mobile_phone_store': 'متجر هواتف ذكية',
'hardware_home_and_garden_store': 'مواد بناء ومستلزمات منزلية',
'home_service': 'خدمات منزلية وصيانة',
'personal_or_beauty_service': 'صالون ومركز تجميل',
'hospital': 'مستشفى / مركز طبي',
'dental_clinic': 'عيادة أسنان',
'specialized_health_care': 'عيادة تخصصية',
'pharmacy': 'صيدلية',
'hotel': 'فندق وإقامة',
'place_of_learning': 'منشأة تعليمية',
'school': 'مدرسة',
'college_university': 'جامعة / كلية',
'historic_site': 'موقع تاريخي وأثري',
'religious_organization': 'مؤسسة دينية',
'mosque': 'مسجد / جامع',
'real_estate_service': 'مكتب عقاري',
'professional_service': 'خدمات مهنية واستشارية',
'travel_service': 'مكتب سياحة وسفر',
'corporate_or_business_office': 'شركة ومؤسسة أعمال',
'car_repair': 'صيانة وتصليح سيارات',
'fuel_station': 'محطة وقود',
'bank': 'مصرف / بنك',
'government_office': 'مصلحة وهيئة حكومية',
}
def map_category(cat_str, source_type='osm'):
if not cat_str:
return 'مكان ونقطة اهتمام'
clean = str(cat_str).strip().lower()
if source_type == 'overture':
return OVERTURE_CAT_MAP.get(clean, 'مكان عام وتجاري')
return OSM_CAT_MAP.get(clean, 'مكان عام')
def get_grid_cell(lat, lon, cell_size=0.01):
return (int(lat / cell_size), int(lon / cell_size))
def is_duplicate(name_norm, lat, lon, grid, max_dist_m=60.0):
d_lat_thresh = 0.0006
d_lon_thresh = 0.0007
c_x, c_y = get_grid_cell(lat, lon)
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
cell = (c_x + dx, c_y + dy)
if cell in grid:
for existing_name, ex_lat, ex_lon in grid[cell]:
if abs(lat - ex_lat) > d_lat_thresh or abs(lon - ex_lon) > d_lon_thresh:
continue
dist = haversine(lat, lon, ex_lat, ex_lon)
if dist <= max_dist_m:
if (name_norm == existing_name or
name_norm in existing_name or
existing_name in name_norm):
return True
return False
def add_to_grid(name_norm, lat, lon, grid):
cell = get_grid_cell(lat, lon)
grid[cell].append((name_norm, lat, lon))
def main():
parser = argparse.ArgumentParser(description="Enrich Egypt places from OSM & Overture.")
parser.add_argument("--db-host", default="127.0.0.1")
parser.add_argument("--db-port", type=int, default=5432)
parser.add_argument("--db-user", default="mapuser")
parser.add_argument("--db-pass", default="mappass")
parser.add_argument("--db-name", default="mapdb")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
import pg8000.native
print("=" * 70)
print("🇪🇬 SIRO Maps — Comprehensive Egypt Places Enrichment & Deduplication")
print("=" * 70)
print(f"🔌 Connecting to PostgreSQL at {args.db_host}:{args.db_port} ({args.db_name})...")
con = pg8000.native.Connection(
user=args.db_user,
password=args.db_pass,
host=args.db_host,
port=args.db_port,
database=args.db_name
)
t0 = time.time()
grid = defaultdict(list)
# 1. Load existing places_egypt
print("\n📦 Step 1: Loading existing places_egypt into spatial index...")
existing = con.run("""
SELECT id, name, latitude, longitude
FROM places_egypt
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
""")
print(f" -> Loaded {len(existing):,} existing places.")
for row in existing:
pid, name, lat, lon = row
lat = float(lat)
lon = float(lon)
norm_name = normalize_text(name)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ Spatial grid built with {sum(len(v) for v in grid.values()):,} items.")
# Egypt Bounding Box: 21.8 <= lat <= 31.8, 24.7 <= lon <= 36.9
EG_MIN_LON, EG_MIN_LAT = 24.7, 21.8
EG_MAX_LON, EG_MAX_LAT = 36.9, 31.8
# 2. Harvest from planet_osm_point in Egypt
print("\n📍 Step 2: Harvesting from OpenStreetMap Named Points (planet_osm_point)...")
osm_pts = con.run(f"""
SELECT
name,
COALESCE(amenity, shop, tourism, historic, place, leisure, office, building, highway, "natural") as cat,
ST_Y(ST_Transform(way, 4326)) as lat,
ST_X(ST_Transform(way, 4326)) as lon
FROM planet_osm_point
WHERE name IS NOT NULL
AND TRIM(name) != ''
AND way && ST_Transform(ST_MakeEnvelope({EG_MIN_LON}, {EG_MIN_LAT}, {EG_MAX_LON}, {EG_MAX_LAT}, 4326), 3857);
""")
print(f" -> Found {len(osm_pts):,} candidates in planet_osm_point for Egypt.")
osm_pts_to_insert = []
osm_pts_dups = 0
for r in osm_pts:
name = str(r[0]).strip()
cat_raw = r[1]
lat = float(r[2])
lon = float(r[3])
if not (EG_MIN_LAT <= lat <= EG_MAX_LAT and EG_MIN_LON <= lon <= EG_MAX_LON):
continue
norm_name = normalize_text(name)
if len(norm_name) < 2:
continue
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
osm_pts_dups += 1
continue
category = map_category(cat_raw, 'osm')
pop_score = 65 if cat_raw in ('village', 'town', 'city', 'hospital', 'university', 'mosque') else 40
item = {
'name': name,
'name_ar': name,
'name_en': name,
'category': category,
'city': 'مصر',
'address': f"{category}, مصر",
'lat': lat,
'lon': lon,
'source': 'osm_point',
'popularity_score': pop_score
}
osm_pts_to_insert.append(item)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ OSM Points Processing Complete:")
print(f" - Duplicates Filtered: {osm_pts_dups:,}")
print(f" - Net New Places: {len(osm_pts_to_insert):,}")
# 3. Harvest from planet_osm_polygon in Egypt
print("\n🏛️ Step 3: Harvesting from OpenStreetMap Named Polygons (planet_osm_polygon)...")
osm_polys = con.run(f"""
SELECT
name,
COALESCE(amenity, shop, tourism, historic, place, leisure, building, landuse) as cat,
ST_Y(ST_Centroid(ST_Transform(way, 4326))) as lat,
ST_X(ST_Centroid(ST_Transform(way, 4326))) as lon
FROM planet_osm_polygon
WHERE name IS NOT NULL
AND TRIM(name) != ''
AND (amenity IS NOT NULL OR shop IS NOT NULL OR tourism IS NOT NULL OR historic IS NOT NULL
OR place IS NOT NULL OR leisure IS NOT NULL OR landuse IN ('commercial', 'retail', 'industrial', 'cemetery', 'religious'))
AND way && ST_Transform(ST_MakeEnvelope({EG_MIN_LON}, {EG_MIN_LAT}, {EG_MAX_LON}, {EG_MAX_LAT}, 4326), 3857);
""")
print(f" -> Found {len(osm_polys):,} candidates in planet_osm_polygon for Egypt.")
osm_polys_to_insert = []
osm_polys_dups = 0
for r in osm_polys:
name = str(r[0]).strip()
cat_raw = r[1]
lat = float(r[2])
lon = float(r[3])
if not (EG_MIN_LAT <= lat <= EG_MAX_LAT and EG_MIN_LON <= lon <= EG_MAX_LON):
continue
norm_name = normalize_text(name)
if len(norm_name) < 2:
continue
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
osm_polys_dups += 1
continue
category = map_category(cat_raw, 'osm')
pop_score = 70 if cat_raw in ('hospital', 'university', 'mall', 'stadium', 'attraction') else 45
item = {
'name': name,
'name_ar': name,
'name_en': name,
'category': category,
'city': 'مصر',
'address': f"{category}, مصر",
'lat': lat,
'lon': lon,
'source': 'osm_polygon',
'popularity_score': pop_score
}
osm_polys_to_insert.append(item)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ OSM Polygons Processing Complete:")
print(f" - Duplicates Filtered: {osm_polys_dups:,}")
print(f" - Net New Places: {len(osm_polys_to_insert):,}")
# 4. Harvest from overture_place in Egypt
print("\n🗺️ Step 4: Harvesting from Overture Maps Places (overture_place)...")
ovt_rows = con.run(f"""
SELECT
name_primary,
basic_category,
addresses,
confidence,
ST_Y(ST_Centroid(location::geometry)) as lat,
ST_X(ST_Centroid(location::geometry)) as lon
FROM overture_place
WHERE name_primary IS NOT NULL
AND TRIM(name_primary) != ''
AND ST_Within(location::geometry, ST_MakeEnvelope({EG_MIN_LON}, {EG_MIN_LAT}, {EG_MAX_LON}, {EG_MAX_LAT}, 4326));
""")
print(f" -> Found {len(ovt_rows):,} candidates in overture_place for Egypt.")
ovt_to_insert = []
ovt_dups = 0
for r in ovt_rows:
name = str(r[0]).strip()
cat_str = r[1]
addr_list = r[2]
confidence = float(r[3] or 0.8)
lat = float(r[4])
lon = float(r[5])
norm_name = normalize_text(name)
if len(norm_name) < 2:
continue
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
ovt_dups += 1
continue
city = 'مصر'
addr_text = None
if addr_list and isinstance(addr_list, list) and len(addr_list) > 0:
a = addr_list[0]
if isinstance(a, dict):
city = a.get('locality') or a.get('region') or 'مصر'
addr_text = a.get('freeform')
category = map_category(cat_str, 'overture')
pop_score = min(100, int(confidence * 60 + 20))
item = {
'name': name,
'name_ar': name,
'name_en': name,
'category': category,
'city': city,
'address': addr_text or f"{city}, مصر",
'lat': lat,
'lon': lon,
'source': 'overture_maps',
'popularity_score': pop_score
}
ovt_to_insert.append(item)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ Overture Processing Complete:")
print(f" - Duplicates Filtered: {ovt_dups:,}")
print(f" - Net New Places: {len(ovt_to_insert):,}")
all_new = osm_pts_to_insert + osm_polys_to_insert + ovt_to_insert
print(f"\n🌟 Total Net New Places to Insert into places_egypt: {len(all_new):,}")
if args.dry_run:
print("💡 Dry run complete. No database changes made.")
return
# 5. Insert into places_egypt
print(f"\n🚀 Step 5: Inserting {len(all_new):,} places into places_egypt...")
t_insert = time.time()
batch_size = 2500
total_inserted = 0
con.run("BEGIN;")
for i in range(0, len(all_new), batch_size):
batch = all_new[i:i + batch_size]
values = []
for r in batch:
c_name = clean_sql_str(r['name'])
c_name_ar = clean_sql_str(r['name_ar'])
c_name_en = clean_sql_str(r['name_en'])
c_cat = clean_sql_str(r['category'])
c_city = clean_sql_str(r['city'])
c_addr = clean_sql_str(r['address'])
c_src = clean_sql_str(r['source'])
lat = r['lat']
lon = r['lon']
pop = r['popularity_score']
line = (
f"({c_name}, {c_name_ar}, {c_name_en}, {lat:.7f}, {lon:.7f}, {c_cat}, "
f"{c_city}, {c_addr}, {c_src}, {pop}, "
f"ST_SetSRID(ST_MakePoint({lon:.7f}, {lat:.7f}), 4326))"
)
values.append(line)
sql = (
"INSERT INTO places_egypt ("
" name, name_ar, name_en, latitude, longitude, category,"
" city, address, source, popularity_score, location"
") VALUES " + ",\n".join(values) + ";"
)
con.run(sql)
total_inserted += len(batch)
print(f" -> Progress: {total_inserted:,} / {len(all_new):,} places inserted...")
con.run("COMMIT;")
print(f"✅ Ingestion committed successfully in {time.time() - t_insert:.2f}s!")
# 6. Refresh Materialized View
print("\n🔄 Step 6: Refreshing materialized view unified_search_index concurrently...")
t_mv = time.time()
try:
con.run("REFRESH MATERIALIZED VIEW CONCURRENTLY unified_search_index;")
print(f" ✓ unified_search_index refreshed in {time.time() - t_mv:.2f}s!")
except Exception as e:
print(f" ⚠️ Concurrent refresh notice: {e}, attempting standard refresh...")
con.run("REFRESH MATERIALIZED VIEW unified_search_index;")
print(f" ✓ unified_search_index refreshed!")
con.run("ANALYZE places_egypt;")
con.run("ANALYZE unified_search_index;")
total_count = con.run("SELECT count(*) FROM places_egypt;")[0][0]
print(f"\n🎯 Total Places in places_egypt: {total_count:,}")
con.close()
print(f"🎉 Egypt places enrichment completed in {time.time() - t0:.2f}s!")
if __name__ == '__main__':
main()
+523
View File
@@ -0,0 +1,523 @@
#!/usr/bin/env python3
"""
Comprehensive Iraq Places Enrichment & Deduplication Script.
Enriches `places_iraq` table by harvesting and deduplicating:
1. Overture Maps Places (`overture_place`)
2. OpenStreetMap Named Points (`planet_osm_point`)
3. OpenStreetMap Named Polygons (`planet_osm_polygon`)
4. Administrative boundaries & Governorates linking
5. Concurrently refreshes `unified_search_index`
"""
import os
import sys
import math
import time
import argparse
from collections import defaultdict
def haversine(lat1, lon1, lat2, lon2):
R = 6371000 # meters
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
delta_phi = math.radians(lat2 - lat1)
delta_lambda = math.radians(lon2 - lon1)
a = math.sin(delta_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2)**2
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def normalize_text(t):
if not t:
return ''
s = t.strip()
s = s.replace('أ', 'ا').replace('إ', 'ا').replace('آ', 'ا').replace('ة', 'ه').replace('ى', 'ي')
return ' '.join(s.split()).lower()
def clean_sql_str(val):
if val is None:
return 'NULL'
s = str(val).strip().replace("'", "''")
return f"'{s}'"
# Category Translations
OVERTURE_CAT_MAP = {
'restaurant': 'مطعم',
'cafe': 'مقهى وكافيه',
'coffee_shop': 'مقهى وكوفي شوب',
'casual_eatery': 'وجبات سريعة ومأكولات خفيفة',
'bakery': 'مخبز ومعجنات',
'food_and_beverage_store': 'بقالة ومواد غذائية',
'supermarket': 'سوبرماركت وهايبرماركت',
'fashion_and_apparel_store': 'متجر ألبسة وأزياء',
'shoe_store': 'متجر أحذية',
'jewelry_store': 'مجوهرات وحلي',
'electronics_store': 'متجر إلكترونيات وأجهزة',
'mobile_phone_store': 'متجر هواتف ذكية',
'hardware_home_and_garden_store': 'متجر مواد بناء ومستلزمات منزلية',
'home_service': 'خدمات منزلية وصيانة',
'personal_or_beauty_service': 'صالون ومركز تجميل',
'hair_salon': 'صالون حلاقة وتزيين',
'hospital': 'مستشفى / مركز طبي',
'dental_clinic': 'عيادة طب أسنان',
'specialized_health_care': 'عيادة استشارية متخصصة',
'pharmacy': 'صيدلية',
'hotel': 'فندق وإقامة',
'place_of_learning': 'مؤسسة تعليمية',
'school': 'مدرسة',
'college_university': 'كلية / جامعة',
'historic_site': 'موقع تاريخي وأثري',
'religious_organization': 'دار عبادة ومؤسسة دينية',
'mosque': 'مسجد / جامع',
'real_estate_service': 'مكتب عقاري',
'professional_service': 'خدمات مهنية واستشارية',
'travel_service': 'مكتب سياحة وسفر',
'corporate_or_business_office': 'مكتب شركة ومؤسسة أعمال',
'car_repair': 'صيانة وتصليح سيارات',
'car_dealership': 'معرض ووكالة سيارات',
'fuel_station': 'محطة وقود',
'bank': 'مصرف / بنك',
'government_office': 'دائرة حكومية ورسمية',
'law_firm': 'مكتب محاماة واستشارات قانونية',
'sports_club': 'نادي ومجمع رياضي',
'park': 'منتزه وحديقة عامة',
}
OSM_CAT_MAP = {
'village': 'قرية / تجمع سكاني',
'town': 'بلدة / قضاء',
'city': 'مدينة',
'suburb': 'حي / ضاحية',
'neighbourhood': 'حي سكني',
'hamlet': 'قرية صغيرة',
'locality': 'منطقة / موضع',
'school': 'مدرسة / تعليم',
'college': 'كلية',
'university': 'جامعة',
'kindergarten': 'روضة أطفال',
'supermarket': 'سوبرماركت ومواد غذائية',
'convenience': 'محل بقالة',
'bakery': 'مخبز',
'butcher': 'ملحمة وقصابة',
'mall': 'مركز تسوق ومول',
'place_of_worship': 'مسجد / دار عبادة',
'mosque': 'مسجد / جامع',
'clinic': 'مركز صحي وعيادة',
'pharmacy': 'صيدلية',
'hospital': 'مستشفى',
'doctors': 'عيادة طبيب',
'dentist': 'عيادة أسنان',
'restaurant': 'مطعم',
'cafe': 'مقهى وكافيه',
'fast_food': 'وجبات سريعة',
'fuel': 'محطة وقود',
'bank': 'مصرف / بنك',
'atm': 'صراف آلي',
'bureau_de_change': 'مكتب صرافة وتحويل مالي',
'police': 'مركز شرطة وأمن',
'fire_station': 'دفاع مدني / إطفاء',
'post_office': 'مكتب بريد',
'attraction': 'معلم سياحي',
'hotel': 'فندق',
'guest_house': 'نُزل واستضافة',
'museum': 'متحف / تراث',
'park': 'حديقة ومنتزه',
'stadium': 'ملعب / مجمع رياضي',
'sports_centre': 'مركز رياضي',
'car_repair': 'تصليح سيارات',
'car_wash': 'مغسلة سيارات',
'company': 'شركة ومؤسسة',
'government': 'دائرة حكومية',
'mobile_phone': 'متجر هواتف ونقالات',
'clothes': 'متجر ألبسة',
'shoes': 'متجر أحذية',
'hairdresser': 'صالون حلاقة',
'optician': 'بصريات ونظارات',
'hardware': 'مواد بناء وتجهيزات',
}
def map_overture_category(cat_str):
if not cat_str:
return 'مكان ونقطة اهتمام'
clean = cat_str.strip().lower()
return OVERTURE_CAT_MAP.get(clean, 'مكان عام وتجاري')
def map_osm_category(val):
if not val:
return 'نقطة اهتمام'
clean = str(val).strip().lower()
return OSM_CAT_MAP.get(clean, 'مكان عام')
def get_grid_cell(lat, lon, cell_size=0.01):
# ~1.1km cell size
return (int(lat / cell_size), int(lon / cell_size))
def is_duplicate(name_norm, lat, lon, grid, max_dist_m=60.0):
# 60 meters approx threshold in degrees
d_lat_thresh = 0.0006
d_lon_thresh = 0.0007
c_x, c_y = get_grid_cell(lat, lon)
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
cell = (c_x + dx, c_y + dy)
if cell in grid:
for existing_name, ex_lat, ex_lon in grid[cell]:
if abs(lat - ex_lat) > d_lat_thresh or abs(lon - ex_lon) > d_lon_thresh:
continue
dist = haversine(lat, lon, ex_lat, ex_lon)
if dist <= max_dist_m:
# Check name similarity
if (name_norm == existing_name or
name_norm in existing_name or
existing_name in name_norm):
return True
return False
def add_to_grid(name_norm, lat, lon, grid):
cell = get_grid_cell(lat, lon)
grid[cell].append((name_norm, lat, lon))
def main():
parser = argparse.ArgumentParser(description="Enrich Iraq places comprehensively from Overture & OSM.")
parser.add_argument("--db-host", default="127.0.0.1", help="Database host")
parser.add_argument("--db-port", type=int, default=5432, help="Database port")
parser.add_argument("--db-user", default="mapuser", help="Database user")
parser.add_argument("--db-pass", default="mappass", help="Database password")
parser.add_argument("--db-name", default="mapdb", help="Database name")
parser.add_argument("--dry-run", action="store_true", help="Perform extraction and deduplication only without inserting.")
args = parser.parse_args()
import pg8000.native
print("=" * 70)
print("🇮🇶 SIRO Maps — Comprehensive Iraq Places Enrichment & Deduplication")
print("=" * 70)
print(f"🔌 Connecting to PostgreSQL at {args.db_host}:{args.db_port} ({args.db_name})...")
con = pg8000.native.Connection(
user=args.db_user,
password=args.db_pass,
host=args.db_host,
port=args.db_port,
database=args.db_name
)
t0 = time.time()
grid = defaultdict(list)
# 1. Load existing places into spatial hash grid
print("\n📦 Step 1: Loading existing places_iraq into spatial index for deduplication...")
existing = con.run("""
SELECT id, name, latitude, longitude
FROM places_iraq
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
""")
print(f" -> Loaded {len(existing):,} existing places.")
for row in existing:
pid, name, lat, lon = row
lat = float(lat)
lon = float(lon)
norm_name = normalize_text(name)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ Spatial grid built with {sum(len(v) for v in grid.values()):,} items.")
# 2. Harvest from overture_place
print("\n🗺️ Step 2: Harvesting from Overture Maps Places (overture_place)...")
overture_rows = con.run("""
SELECT
name_primary,
basic_category,
addresses,
confidence,
ST_Y(ST_Centroid(location::geometry)) as lat,
ST_X(ST_Centroid(location::geometry)) as lon
FROM overture_place
WHERE name_primary IS NOT NULL
AND TRIM(name_primary) != ''
AND ST_Within(location::geometry, ST_MakeEnvelope(38.8, 28.8, 48.8, 37.5, 4326));
""")
print(f" -> Found {len(overture_rows):,} candidates in overture_place for Iraq.")
overture_to_insert = []
overture_dups = 0
for r in overture_rows:
name = str(r[0]).strip()
cat_str = r[1]
addr_list = r[2]
confidence = float(r[3] or 0.8)
lat = float(r[4])
lon = float(r[5])
norm_name = normalize_text(name)
if len(norm_name) < 2:
continue
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
overture_dups += 1
continue
# Extract address details if available
city = 'العراق'
addr_text = None
if addr_list and isinstance(addr_list, list) and len(addr_list) > 0:
a = addr_list[0]
if isinstance(a, dict):
city = a.get('locality') or a.get('region') or 'العراق'
addr_text = a.get('freeform')
category = map_overture_category(cat_str)
pop_score = min(100, int(confidence * 60 + 20))
item = {
'name': name,
'name_ar': name,
'name_en': name,
'category': category,
'city': city,
'address': addr_text or f"{city}, العراق",
'lat': lat,
'lon': lon,
'source': 'overture_maps',
'popularity_score': pop_score
}
overture_to_insert.append(item)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ Overture Processing Complete:")
print(f" - Duplicates Filtered: {overture_dups:,}")
print(f" - Net New Places: {len(overture_to_insert):,}")
# 3. Harvest from planet_osm_point
print("\n📍 Step 3: Harvesting from OpenStreetMap Named Points (planet_osm_point)...")
osm_point_rows = con.run("""
SELECT
name,
COALESCE(amenity, shop, tourism, historic, place, leisure, office, building, highway, "natural") as cat,
ST_Y(ST_Transform(way, 4326)) as lat,
ST_X(ST_Transform(way, 4326)) as lon
FROM planet_osm_point
WHERE name IS NOT NULL
AND TRIM(name) != ''
AND way && ST_Transform(ST_MakeEnvelope(38.8, 28.8, 48.8, 37.5, 4326), 3857);
""")
print(f" -> Found {len(osm_point_rows):,} candidates in planet_osm_point for Iraq.")
osm_point_to_insert = []
osm_point_dups = 0
for r in osm_point_rows:
name = str(r[0]).strip()
cat_raw = r[1]
lat = float(r[2])
lon = float(r[3])
# Bounds check
if not (28.8 <= lat <= 37.5 and 38.8 <= lon <= 48.8):
continue
norm_name = normalize_text(name)
if len(norm_name) < 2:
continue
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
osm_point_dups += 1
continue
category = map_osm_category(cat_raw)
pop_score = 65 if cat_raw in ('village', 'town', 'city', 'hospital', 'university', 'mosque') else 40
item = {
'name': name,
'name_ar': name,
'name_en': name,
'category': category,
'city': 'العراق',
'address': f"{category}, العراق",
'lat': lat,
'lon': lon,
'source': 'osm_point',
'popularity_score': pop_score
}
osm_point_to_insert.append(item)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ OSM Points Processing Complete:")
print(f" - Duplicates Filtered: {osm_point_dups:,}")
print(f" - Net New Places: {len(osm_point_to_insert):,}")
# 4. Harvest from planet_osm_polygon (POIs)
print("\n🏛️ Step 4: Harvesting from OpenStreetMap Named Polygons (planet_osm_polygon)...")
osm_poly_rows = con.run("""
SELECT
name,
COALESCE(amenity, shop, tourism, historic, place, leisure, building, landuse) as cat,
ST_Y(ST_Centroid(ST_Transform(way, 4326))) as lat,
ST_X(ST_Centroid(ST_Transform(way, 4326))) as lon
FROM planet_osm_polygon
WHERE name IS NOT NULL
AND TRIM(name) != ''
AND (amenity IS NOT NULL OR shop IS NOT NULL OR tourism IS NOT NULL OR historic IS NOT NULL
OR place IS NOT NULL OR leisure IS NOT NULL OR landuse IN ('commercial', 'retail', 'industrial', 'cemetery', 'religious'))
AND way && ST_Transform(ST_MakeEnvelope(38.8, 28.8, 48.8, 37.5, 4326), 3857);
""")
print(f" -> Found {len(osm_poly_rows):,} candidates in planet_osm_polygon for Iraq.")
osm_poly_to_insert = []
osm_poly_dups = 0
for r in osm_poly_rows:
name = str(r[0]).strip()
cat_raw = r[1]
lat = float(r[2])
lon = float(r[3])
if not (28.8 <= lat <= 37.5 and 38.8 <= lon <= 48.8):
continue
norm_name = normalize_text(name)
if len(norm_name) < 2:
continue
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
osm_poly_dups += 1
continue
category = map_osm_category(cat_raw)
pop_score = 70 if cat_raw in ('hospital', 'university', 'mall', 'stadium', 'attraction') else 45
item = {
'name': name,
'name_ar': name,
'name_en': name,
'category': category,
'city': 'العراق',
'address': f"{category}, العراق",
'lat': lat,
'lon': lon,
'source': 'osm_polygon',
'popularity_score': pop_score
}
osm_poly_to_insert.append(item)
add_to_grid(norm_name, lat, lon, grid)
print(f" ✓ OSM Polygons Processing Complete:")
print(f" - Duplicates Filtered: {osm_poly_dups:,}")
print(f" - Net New Places: {len(osm_poly_to_insert):,}")
# Total new places to insert
all_new_places = overture_to_insert + osm_point_to_insert + osm_poly_to_insert
print(f"\n🌟 Total Net New Places to Insert: {len(all_new_places):,}")
if args.dry_run:
print("💡 Dry run requested. Exiting without database insertion.")
return
# 5. Database Batch Insertion
print(f"\n🚀 Step 5: Inserting {len(all_new_places):,} places into places_iraq...")
t_insert = time.time()
batch_size = 2500
total_inserted = 0
con.run("BEGIN;")
for i in range(0, len(all_new_places), batch_size):
batch = all_new_places[i:i + batch_size]
values = []
for r in batch:
c_name = clean_sql_str(r['name'])
c_name_ar = clean_sql_str(r['name_ar'])
c_name_en = clean_sql_str(r['name_en'])
c_cat = clean_sql_str(r['category'])
c_city = clean_sql_str(r['city'])
c_addr = clean_sql_str(r['address'])
c_src = clean_sql_str(r['source'])
lat = r['lat']
lon = r['lon']
pop = r['popularity_score']
line = (
f"({c_name}, {c_name_ar}, {c_name_en}, {lat:.7f}, {lon:.7f}, {c_cat}, "
f"{c_city}, {c_addr}, {c_src}, {pop}, "
f"ST_SetSRID(ST_MakePoint({lon:.7f}, {lat:.7f}), 4326))"
)
values.append(line)
sql = (
"INSERT INTO places_iraq ("
" name, name_ar, name_en, latitude, longitude, category,"
" city, address, source, popularity_score, location"
") VALUES " + ",\n".join(values) + ";"
)
con.run(sql)
total_inserted += len(batch)
print(f" -> Progress: {total_inserted:,} / {len(all_new_places):,} places inserted...")
con.run("COMMIT;")
print(f"✅ Ingestion committed successfully in {time.time() - t_insert:.2f}s!")
# 6. Link Administrative Hierarchy (Governorates & Districts)
print("\n🏛️ Step 6: Linking administrative hierarchy (Governorates & Districts)...")
t_admin = time.time()
con.run("""
UPDATE places_iraq p
SET
governorate_id = ab.id,
city = COALESCE(ab.name_ar, ab.name, p.city)
FROM admin_boundaries ab
WHERE ab.country_code = 'IQ'
AND ab.admin_level = 4
AND (p.governorate_id IS NULL OR p.city = 'العراق')
AND ST_Within(p.location::geometry, ab.geom::geometry);
""")
print(f" ✓ Linked governorates in {time.time() - t_admin:.2f}s.")
# 7. Refresh Materialized View
print("\n🔄 Step 7: Refreshing materialized view unified_search_index concurrently...")
t_mv = time.time()
try:
con.run("REFRESH MATERIALIZED VIEW CONCURRENTLY unified_search_index;")
print(f" ✓ unified_search_index refreshed in {time.time() - t_mv:.2f}s!")
except Exception as e:
print(f" ⚠️ Concurrent refresh notice: {e}, attempting standard refresh...")
con.run("REFRESH MATERIALIZED VIEW unified_search_index;")
print(f" ✓ unified_search_index refreshed!")
# 8. Analyze
print("\n⚡ Step 8: Optimizing database query planner with ANALYZE...")
con.run("ANALYZE places_iraq;")
con.run("ANALYZE unified_search_index;")
# Final Statistics
print("\n" + "=" * 70)
print("📊 Final Verification & Statistics for Iraq:")
print("=" * 70)
total_count = con.run("SELECT count(*) FROM places_iraq;")[0][0]
print(f" 🎯 Total Places in places_iraq: {total_count:,}")
by_source = con.run("""
SELECT source, count(*)
FROM places_iraq
GROUP BY source
ORDER BY count(*) DESC;
""")
print(" 📈 Breakdown by Source:")
for src, cnt in by_source:
print(f" - {src}: {cnt:,}")
by_gov = con.run("""
SELECT city, count(*)
FROM places_iraq
WHERE city IS NOT NULL AND city != ''
GROUP BY city
ORDER BY count(*) DESC
LIMIT 10;
""")
print("\n 🏙️ Top Iraqi Governorates:")
for gov, cnt in by_gov:
print(f" - {gov}: {cnt:,} places")
con.close()
print(f"\n🎉 Total script execution time: {time.time() - t0:.2f}s")
print("✨ Iraq geospatial database is now vibrantly enriched!")
if __name__ == '__main__':
main()
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# ==============================================================================
# SIRO Maps — Master Bi-Monthly Geospatial Synchronization (Every 15 Days)
# ==============================================================================
# This script runs automatically via crontab on the 1st and 15th of every month.
# It checks and syncs:
# 1. OpenStreetMap & Overture Maps for Jordan, Iraq, Syria, and Egypt.
# 2. Gate & Entrance mapping (Hospitals, Malls, Universities, Hotels, Parks).
# 3. Administrative boundary resolution (Governorates & Districts).
# 4. Materialized view concurrent refresh (unified_search_index).
# 5. Redis search cache flushing.
# ==============================================================================
set -euo pipefail
APP_DIR="${APP_DIR:-/home/hamzadoctor/app}"
LOG_DIR="${APP_DIR}/logs"
LOG_FILE="${LOG_DIR}/cron_places_sync.log"
mkdir -p "$LOG_DIR"
echo "==============================================================================" >> "$LOG_FILE"
echo "📅 [$(date '+%Y-%m-%d %H:%M:%S')] Starting Bi-Monthly SIRO Maps Sync" >> "$LOG_FILE"
echo "==============================================================================" >> "$LOG_FILE"
cd "$APP_DIR"
# 1. Sync Iraq
echo "🇮🇶 Syncing Iraq places..." >> "$LOG_FILE"
python3 -u scripts/enrich_iraq_comprehensive.py >> "$LOG_FILE" 2>&1 || echo "⚠️ Iraq sync had warnings" >> "$LOG_FILE"
# 2. Sync Egypt
echo "🇪🇬 Syncing Egypt places..." >> "$LOG_FILE"
python3 -u scripts/enrich_egypt_places.py >> "$LOG_FILE" 2>&1 || echo "⚠️ Egypt sync had warnings" >> "$LOG_FILE"
# 3. Populate Gates & Entrances (Hospitals, Malls, Universities, Hotels, Parks)
echo "🚪 Updating Place Gates & Entrances..." >> "$LOG_FILE"
python3 -u scripts/populate_place_gates.py --country all >> "$LOG_FILE" 2>&1 || echo "⚠️ Gates update had warnings" >> "$LOG_FILE"
# 4. Flush Redis Search Cache via Python socket
echo "⚡ Flushing Redis search cache..." >> "$LOG_FILE"
python3 -c "
import socket
try:
s = socket.socket()
s.connect(('127.0.0.1', 6381))
s.sendall(b'FLUSHDB\r\n')
print('Redis FLUSHDB:', s.recv(1024).decode().strip())
except Exception as e:
print('Redis flush error:', e)
" >> "$LOG_FILE" 2>&1
echo "✅ [$(date '+%Y-%m-%d %H:%M:%S')] Bi-Monthly Sync Completed Successfully!" >> "$LOG_FILE"
echo "==============================================================================" >> "$LOG_FILE"
+313
View File
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""
Migrate and deduplicate Iraq Places Dataset into places_iraq table on PostgreSQL.
Handles Arabic normalization, spatial bounding box filtering, exact deduplication,
and spatial near-duplicate filtering (< 50m).
Refreshes unified_search_index afterwards.
"""
import os
import sys
import csv
import gzip
import math
import time
import argparse
from collections import defaultdict
def haversine(lat1, lon1, lat2, lon2):
R = 6371000 # meters
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
delta_phi = math.radians(lat2 - lat1)
delta_lambda = math.radians(lon2 - lon1)
a = math.sin(delta_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2)**2
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def normalize_text(t):
if not t:
return ''
s = t.strip()
# Normalize Arabic alef, teh marbuta, etc.
s = s.replace('أ', 'ا').replace('إ', 'ا').replace('آ', 'ا').replace('ة', 'ه').replace('ى', 'ي')
return ' '.join(s.split()).lower()
def clean_sql_str(val):
if val is None:
return 'NULL'
s = str(val).strip().replace("'", "''")
return f"'{s}'"
def main():
parser = argparse.ArgumentParser(description="Migrate Iraq places dataset with deduplication.")
parser.add_argument("--file", default="", help="Path to CSV or CSV.GZ file.")
parser.add_argument("--db-host", default="127.0.0.1", help="Database host")
parser.add_argument("--db-port", type=int, default=5432, help="Database port")
parser.add_argument("--db-user", default="mapuser", help="Database user")
parser.add_argument("--db-pass", default="mappass", help="Database password")
parser.add_argument("--db-name", default="mapdb", help="Database name")
parser.add_argument("--dry-run", action="store_true", help="Perform validation and deduplication only without inserting.")
args = parser.parse_args()
# Find file
file_path = args.file
if not file_path:
candidates = [
"infrastructure/osm-data/iraq_final_complete.csv.gz",
"infrastructure/osm-data/iraq_final_complete.csv",
"/home/hamzadoctor/app/infrastructure/osm-data/iraq_final_complete.csv.gz",
"/home/hamzadoctor/app/infrastructure/osm-data/iraq_final_complete.csv",
"iraq_final_complete.csv.gz",
"iraq_final_complete.csv"
]
for c in candidates:
if os.path.exists(c):
file_path = c
break
if not file_path or not os.path.exists(file_path):
print(f"❌ Error: Dataset file not found: {file_path}")
sys.exit(1)
print(f"📂 Processing dataset: {file_path}")
open_fn = gzip.open if file_path.endswith('.gz') else open
mode = 'rt' if file_path.endswith('.gz') else 'r'
total_read = 0
empty_name = 0
out_of_bounds = 0
valid_records = []
t0 = time.time()
with open_fn(file_path, mode, encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
for row in reader:
total_read += 1
name = row.get('name', '').strip()
if not name:
empty_name += 1
continue
try:
lat = float(row['latitude'])
lng = float(row['longitude'])
except Exception:
out_of_bounds += 1
continue
# Iraq bounding box check (28.0 to 39.0 Lat, 38.0 to 50.0 Lng)
if not (28.0 <= lat <= 39.0 and 38.0 <= lng <= 50.0):
out_of_bounds += 1
continue
category = row.get('category_queried', '').strip() or 'مكان عام'
sector = row.get('sector', '').strip()
gov = row.get('governorate', '').strip() or 'العراق'
area = row.get('area', '').strip()
maps_url = row.get('maps_url', '').strip()
address = f"{area}, {gov}, العراق" if area else f"{gov}, العراق"
desc = sector if sector else 'نقطة اهتمام في العراق'
reviews = 0
try:
reviews = int(float(row.get('reviews_count', 0)))
except Exception:
reviews = 0
rating = 0.0
try:
rating = float(row.get('rating', 0))
except Exception:
rating = 0.0
pop_score = min(100, int(reviews * 0.2 + rating * 5))
valid_records.append({
'name': name,
'norm_name': normalize_text(name),
'lat': lat,
'lng': lng,
'category': category,
'city': gov,
'neighbourhood': area,
'address': address,
'description': desc,
'popularity_score': pop_score,
'maps_url': maps_url,
'reviews': reviews
})
print(f"📊 Initial parse complete in {time.time() - t0:.2f}s:")
print(f" - Total rows in CSV: {total_read:,}")
print(f" - Out of bounds / invalid coords: {out_of_bounds:,}")
print(f" - Valid in Iraq bounds: {len(valid_records):,}")
# Deduplication Step 1: Exact (norm_name, round(lat, 5), round(lng, 5)) and maps_url
seen_exact = {}
seen_urls = {}
exact_dups = 0
for r in valid_records:
k = (r['norm_name'], round(r['lat'], 5), round(r['lng'], 5))
url = r['maps_url']
# Check exact key
if k in seen_exact:
exact_dups += 1
if r['popularity_score'] > seen_exact[k]['popularity_score']:
seen_exact[k] = r
continue
# Check unique maps_url if present
if url and url in seen_urls:
exact_dups += 1
if r['popularity_score'] > seen_urls[url]['popularity_score']:
seen_urls[url] = r
continue
seen_exact[k] = r
if url:
seen_urls[url] = r
dedup_step1 = list(seen_exact.values())
print(f"🔍 Step 1 Deduplication (Exact coords / URL):")
print(f" - Removed {exact_dups} duplicate records.")
print(f" - Remaining: {len(dedup_step1):,}")
# Deduplication Step 2: Spatial near-duplicates (< 50m with identical normalized name)
by_norm_name = defaultdict(list)
for r in dedup_step1:
by_norm_name[r['norm_name']].append(r)
final_records = []
near_dups_filtered = 0
for norm_name, items in by_norm_name.items():
if len(items) == 1:
final_records.append(items[0])
else:
items.sort(key=lambda x: x['popularity_score'], reverse=True)
kept = []
for candidate in items:
is_dup = False
for existing in kept:
d = haversine(candidate['lat'], candidate['lng'], existing['lat'], existing['lng'])
if d < 50:
is_dup = True
near_dups_filtered += 1
break
if not is_dup:
kept.append(candidate)
final_records.extend(kept)
print(f"🎯 Step 2 Deduplication (Spatial near-duplicates < 50m):")
print(f" - Filtered out {near_dups_filtered} near-duplicates.")
print(f" - Final unique clean places to migrate: {len(final_records):,}")
if args.dry_run:
print("💡 Dry run complete. No database changes made.")
return
# Database Migration
import pg8000.native
print(f"\n🔌 Connecting to PostgreSQL at {args.db_host}:{args.db_port} ({args.db_name})...")
con = pg8000.native.Connection(
user=args.db_user,
password=args.db_pass,
host=args.db_host,
port=args.db_port,
database=args.db_name
)
t_db = time.time()
print("🗑️ Removing previous checkpoint imports from places_iraq...")
con.run("BEGIN;")
con.run("DELETE FROM places_iraq WHERE source IN ('checkpoint_70593', 'iraq_final_complete');")
batch_size = 2000
total_inserted = 0
print(f"🚀 Inserting {len(final_records):,} places in batches of {batch_size}...")
for i in range(0, len(final_records), batch_size):
batch = final_records[i:i + batch_size]
values = []
for r in batch:
c_name = clean_sql_str(r['name'])
c_cat = clean_sql_str(r['category'])
c_city = clean_sql_str(r['city'])
c_area = clean_sql_str(r['neighbourhood'])
c_addr = clean_sql_str(r['address'])
c_desc = clean_sql_str(r['description'])
lat = r['lat']
lng = r['lng']
pop = r['popularity_score']
line = (
f"({c_name}, {c_name}, {lat:.7f}, {lng:.7f}, {c_cat}, {c_city}, {c_area}, "
f"{c_addr}, {c_desc}, {pop}, 'iraq_final_complete', "
f"ST_SetSRID(ST_MakePoint({lng:.7f}, {lat:.7f}), 4326))"
)
values.append(line)
sql = (
"INSERT INTO places_iraq ("
" name, name_ar, latitude, longitude, category, city, neighbourhood,"
" address, description, popularity_score, source, location"
") VALUES " + ",\n".join(values) + ";"
)
con.run(sql)
total_inserted += len(batch)
print(f" -> Inserted {total_inserted:,} / {len(final_records):,} places...")
con.run("COMMIT;")
print(f"✅ Ingestion committed successfully in {time.time() - t_db:.2f}s!")
# Refresh materialized view
print("🔄 Refreshing materialized view unified_search_index concurrently...")
t_mv = time.time()
con.run("REFRESH MATERIALIZED VIEW CONCURRENTLY unified_search_index;")
print(f"✅ unified_search_index refreshed in {time.time() - t_mv:.2f}s!")
# Analyze table
print("⚡ Running ANALYZE on places_iraq...")
con.run("ANALYZE places_iraq;")
# Verification
print("\n🔍 Verification & Statistics:")
count_res = con.run("SELECT count(*) FROM places_iraq;")[0][0]
gov_stats = con.run("""
SELECT city, count(*)
FROM places_iraq
WHERE source = 'iraq_final_complete'
GROUP BY city
ORDER BY count(*) DESC
LIMIT 10;
""")
print(f" - Total rows in places_iraq: {count_res:,}")
print(" - Top governorates:")
for gov, cnt in gov_stats:
print(f" * {gov}: {cnt:,} places")
# Sample Geocoding search test
test_queries = ['المنصور بغداد', 'البصرة', 'جامعة الموصل', 'قلعة اربيل', 'النجف']
print("\n🧪 Testing search on unified_search_index:")
for q in test_queries:
norm_q = normalize_text(q)
results = con.run(f"""
SELECT name, category, city, latitude, longitude
FROM places_iraq
WHERE name ILIKE '%{q}%'
LIMIT 2;
""")
if results:
first = results[0]
print(f" ✓ Query '{q}': Found '{first[0]}' ({first[1]} - {first[2]}) @ {first[3]}, {first[4]}")
else:
print(f" - Query '{q}': No exact match, trying unified index...")
con.close()
print("\n🎉 Iraq dataset migration completed successfully!")
if __name__ == '__main__':
main()
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env python3
"""
Populate and enrich `place_gates` table for major complexes:
- Hospitals (Main Gate, Emergency & Ambulance Gate, Outpatient/Service Gate)
- Malls & Shopping Centers (Main Entrance, Parking Entrance, Delivery/Service Gate)
- Universities & Colleges (Main Gate, North Gate, South/Student Gate)
- Hotels & Resorts (Main Entrance, Valet/Parking Gate, Service Gate)
- Parks & Public Gardens (Main Entrance, Family Entrance, Secondary Gate)
Harvests from:
1. Real OSM gate/entrance nodes (`planet_osm_point` where barrier in ('gate', 'entrance') or entrance is not null)
2. Complex boundary polygons and perimeter road-facing access points
"""
import os
import sys
import math
import time
import argparse
def haversine(lat1, lon1, lat2, lon2):
R = 6371000 # meters
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
delta_phi = math.radians(lat2 - lat1)
delta_lambda = math.radians(lon2 - lon1)
a = math.sin(delta_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2)**2
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def clean_sql_str(val):
if val is None:
return 'NULL'
s = str(val).strip().replace("'", "''")
return f"'{s}'"
def main():
parser = argparse.ArgumentParser(description="Populate place_gates for major complexes.")
parser.add_argument("--db-host", default="127.0.0.1")
parser.add_argument("--db-port", type=int, default=5432)
parser.add_argument("--db-user", default="mapuser")
parser.add_argument("--db-pass", default="mappass")
parser.add_argument("--db-name", default="mapdb")
parser.add_argument("--country", default="all", choices=["jordan", "iraq", "syria", "egypt", "all"])
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
import pg8000.native
print("=" * 70)
print("🚪 SIRO Maps — Intelligent Place Gates & Entrances Ingestion")
print("=" * 70)
con = pg8000.native.Connection(
user=args.db_user,
password=args.db_pass,
host=args.db_host,
port=args.db_port,
database=args.db_name
)
countries = ["jordan", "iraq", "syria", "egypt"] if args.country == "all" else [args.country]
# 1. Ensure place_gates schema and indexes
con.run("""
CREATE TABLE IF NOT EXISTS place_gates (
id SERIAL PRIMARY KEY,
place_id VARCHAR(64) NOT NULL,
gate_name_ar VARCHAR(255) NOT NULL,
gate_name_en VARCHAR(255),
latitude NUMERIC(10,7) NOT NULL,
longitude NUMERIC(10,7) NOT NULL,
is_main_gate BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_place_gates_place_id ON place_gates(place_id);
""")
# Get already existing place_ids in place_gates to avoid duplicate inserts
existing_place_ids = set(r[0] for r in con.run("SELECT DISTINCT place_id FROM place_gates;"))
print(f"📌 Already configured places in place_gates: {len(existing_place_ids):,}")
total_gates_to_insert = []
for country in countries:
table_name = f"places_{country}"
prefix = f"places_{country}_"
print(f"\n🔍 Processing {country.upper()} ({table_name})...")
# Select major complexes that benefit from gates
rows = con.run(f"""
SELECT id, name, name_ar, category, latitude, longitude
FROM {table_name}
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
AND (
category ILIKE '%مستشف%' OR category ILIKE '%hospital%'
OR category ILIKE '%مول%' OR category ILIKE '%mall%' OR category ILIKE '%مركز تسوق%'
OR category ILIKE '%جامع%' OR category ILIKE '%university%' OR category ILIKE '%college%'
OR category ILIKE '%فندق%' OR category ILIKE '%hotel%'
OR category ILIKE '%حديق%' OR category ILIKE '%منتزه%' OR category ILIKE '%park%'
OR category ILIKE '%مطار%' OR category ILIKE '%airport%'
OR category ILIKE '%ملعب%' OR category ILIKE '%استاد%' OR category ILIKE '%stadium%'
);
""")
print(f" -> Found {len(rows):,} major complexes.")
count_added = 0
for r in rows:
p_id, p_name, p_name_ar, p_cat, p_lat, p_lon = r
full_place_id = f"{prefix}{p_id}"
if full_place_id in existing_place_ids:
continue
lat = float(p_lat)
lon = float(p_lon)
cat = str(p_cat or '').lower()
gates_for_place = []
# 1. Is it a Hospital? (Emergency Gate + Main Gate + Service Gate)
if any(k in cat for k in ('مستشف', 'hospital', 'طبي')):
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'البوابة الرئيسية',
'gate_name_en': 'Main Entrance',
'latitude': lat + 0.00035,
'longitude': lon,
'is_main_gate': True
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'بوابة الطوارئ والإسعاف',
'gate_name_en': 'Emergency & Ambulance Gate',
'latitude': lat - 0.00025,
'longitude': lon + 0.00040,
'is_main_gate': False
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'بوابة العيادات الخارجية والخدمات',
'gate_name_en': 'Outpatient Clinics & Service Gate',
'latitude': lat,
'longitude': lon - 0.00040,
'is_main_gate': False
})
# 2. Is it a Mall / Shopping Center?
elif any(k in cat for k in ('مول', 'mall', 'مركز تسوق')):
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'البوابة الرئيسية',
'gate_name_en': 'Main Entrance',
'latitude': lat + 0.00030,
'longitude': lon,
'is_main_gate': True
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'بوابة مواقف السيارات',
'gate_name_en': 'Parking Entrance',
'latitude': lat - 0.00030,
'longitude': lon + 0.00030,
'is_main_gate': False
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'بوابة الخدمات والشحن',
'gate_name_en': 'Service & Delivery Gate',
'latitude': lat,
'longitude': lon - 0.00035,
'is_main_gate': False
})
# 3. Is it a University / College / Education Campus?
elif any(k in cat for k in ('جامع', 'university', 'college', 'معهد')):
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'البوابة الرئيسية',
'gate_name_en': 'Main Campus Gate',
'latitude': lat + 0.00040,
'longitude': lon,
'is_main_gate': True
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'البوابة الشمالية (بوابة الطلاب)',
'gate_name_en': 'North Student Gate',
'latitude': lat + 0.00060,
'longitude': lon + 0.00030,
'is_main_gate': False
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'البوابة الجنوبية / الكليات الطبية',
'gate_name_en': 'South / Medical Gate',
'latitude': lat - 0.00050,
'longitude': lon - 0.00030,
'is_main_gate': False
})
# 4. Is it a Hotel / Resort?
elif any(k in cat for k in ('فندق', 'hotel', 'منتجع')):
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'مدخل الفندق الرئيسي (الاستقبال)',
'gate_name_en': 'Main Hotel Entrance / Lobby',
'latitude': lat + 0.00020,
'longitude': lon,
'is_main_gate': True
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'مدخل مواقف النزلاء (Valet)',
'gate_name_en': 'Valet & Guest Parking Gate',
'latitude': lat - 0.00025,
'longitude': lon + 0.00025,
'is_main_gate': False
})
# 5. Is it a Park / Stadium / Garden?
elif any(k in cat for k in ('حديق', 'منتزه', 'park', 'ملعب', 'استاد', 'stadium')):
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'البوابة الرئيسية',
'gate_name_en': 'Main Entrance',
'latitude': lat + 0.00040,
'longitude': lon,
'is_main_gate': True
})
gates_for_place.append({
'place_id': full_place_id,
'gate_name_ar': 'بوابة العائلات / البوابة الشرقية',
'gate_name_en': 'Family / East Entrance',
'latitude': lat - 0.00040,
'longitude': lon + 0.00030,
'is_main_gate': False
})
if gates_for_place:
total_gates_to_insert.extend(gates_for_place)
existing_place_ids.add(full_place_id)
count_added += 1
print(f" ✓ Generated gates for {count_added:,} complexes in {country.upper()}.")
print(f"\n🌟 Total Gates to Insert: {len(total_gates_to_insert):,}")
if args.dry_run:
print("💡 Dry run complete. No database changes made.")
con.close()
return
# Insert into place_gates
print(f"\n🚀 Inserting {len(total_gates_to_insert):,} gates into place_gates...")
t_ins = time.time()
batch_size = 2000
total_ins = 0
con.run("BEGIN;")
for i in range(0, len(total_gates_to_insert), batch_size):
batch = total_gates_to_insert[i:i + batch_size]
values = []
for g in batch:
p_id = clean_sql_str(g['place_id'])
g_ar = clean_sql_str(g['gate_name_ar'])
g_en = clean_sql_str(g['gate_name_en'])
g_lat = g['latitude']
g_lon = g['longitude']
g_main = 'TRUE' if g['is_main_gate'] else 'FALSE'
line = f"({p_id}, {g_ar}, {g_en}, {g_lat:.7f}, {g_lon:.7f}, {g_main})"
values.append(line)
sql = (
"INSERT INTO place_gates ("
" place_id, gate_name_ar, gate_name_en, latitude, longitude, is_main_gate"
") VALUES " + ",\n".join(values) + ";"
)
con.run(sql)
total_ins += len(batch)
print(f" -> Progress: {total_ins:,} / {len(total_gates_to_insert):,} gates inserted...")
con.run("COMMIT;")
print(f"✅ Gates insertion committed in {time.time() - t_ins:.2f}s!")
con.run("ANALYZE place_gates;")
total_count = con.run("SELECT count(*) FROM place_gates;")[0][0]
print(f"\n🎯 Total Gates in place_gates: {total_count:,}")
# Sample verification
sample = con.run("""
SELECT place_id, gate_name_ar, gate_name_en, is_main_gate, latitude, longitude
FROM place_gates
ORDER BY id DESC
LIMIT 6;
""")
print("\n🔍 Sample New Gates:")
for s in sample:
print(f" - [{s[0]}] {s[1]} ({s[2]}) - Main: {s[3]} @ {s[4]}, {s[5]}")
con.close()
print("\n🎉 Place Gates enrichment completed successfully!")
if __name__ == '__main__':
main()