/** * GoodGo Platform — POI Seed Data * * Seeds Points of Interest (POIs) for HCMC with realistic coordinates. * Covers: schools, hospitals, metro stations, malls, parks, police stations. * Also computes initial NeighborhoodScore for each district. * * Usage: called from prisma/seed.ts or standalone via `tsx scripts/seed-pois.ts` */ import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@prisma/client'; import pg from 'pg'; // POI type matching the Prisma enum type POIType = | 'SCHOOL' | 'UNIVERSITY' | 'HOSPITAL' | 'CLINIC' | 'METRO_STATION' | 'BUS_STOP' | 'MALL' | 'MARKET' | 'SUPERMARKET' | 'PARK' | 'POLICE_STATION' | 'FIRE_STATION' | 'BANK' | 'ATM' | 'RESTAURANT' | 'CAFE' | 'GYM' | 'PHARMACY'; interface POISeed { id: string; name: string; type: POIType; lat: number; lng: number; address?: string; ward?: string; district: string; city: string; osmId?: string; } // ─── HCMC POI Data ────────────────────────────────────────────────────────── const HCMC_POIS: POISeed[] = [ // ── Schools ── { id: 'poi-school-001', name: 'Trường THPT Lê Quý Đôn', type: 'SCHOOL', lat: 10.7808, lng: 106.6942, address: '110 Nguyễn Thị Minh Khai', ward: 'Phường 6', district: 'Quận 3', city: 'Hồ Chí Minh', osmId: 'node/1001' }, { id: 'poi-school-002', name: 'Trường THPT Trần Đại Nghĩa', type: 'SCHOOL', lat: 10.7693, lng: 106.6788, address: '20 Lý Tự Trọng', ward: 'Bến Nghé', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/1002' }, { id: 'poi-school-003', name: 'Trường Quốc tế Anh Việt (BVIS)', type: 'SCHOOL', lat: 10.8018, lng: 106.7362, address: '246 Nguyễn Văn Hưởng', ward: 'Thảo Điền', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/1003' }, { id: 'poi-school-004', name: 'Vinschool Central Park', type: 'SCHOOL', lat: 10.7935, lng: 106.7205, address: 'Vinhomes Central Park', ward: 'Phường 22', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/1004' }, { id: 'poi-school-005', name: 'Trường THPT Nguyễn Thượng Hiền', type: 'SCHOOL', lat: 10.7870, lng: 106.6635, address: '4 Nguyễn Thượng Hiền', ward: 'Phường 5', district: 'Quận 3', city: 'Hồ Chí Minh', osmId: 'node/1005' }, { id: 'poi-school-006', name: 'Trường Quốc tế Sài Gòn (SIS)', type: 'SCHOOL', lat: 10.7293, lng: 106.7198, address: 'Khu Phú Mỹ Hưng', ward: 'Tân Phú', district: 'Quận 7', city: 'Hồ Chí Minh', osmId: 'node/1006' }, // ── Universities ── { id: 'poi-uni-001', name: 'Đại học Bách Khoa TP.HCM', type: 'UNIVERSITY', lat: 10.7723, lng: 106.6577, address: '268 Lý Thường Kiệt', ward: 'Phường 14', district: 'Quận 10', city: 'Hồ Chí Minh', osmId: 'node/2001' }, { id: 'poi-uni-002', name: 'Đại học RMIT Việt Nam', type: 'UNIVERSITY', lat: 10.7290, lng: 106.6951, address: '702 Nguyễn Văn Linh', ward: 'Tân Hưng', district: 'Quận 7', city: 'Hồ Chí Minh', osmId: 'node/2002' }, { id: 'poi-uni-003', name: 'Đại học Kinh tế TP.HCM (UEH)', type: 'UNIVERSITY', lat: 10.7840, lng: 106.6956, address: '59C Nguyễn Đình Chiểu', ward: 'Phường 6', district: 'Quận 3', city: 'Hồ Chí Minh', osmId: 'node/2003' }, // ── Hospitals ── { id: 'poi-hospital-001', name: 'Bệnh viện Chợ Rẫy', type: 'HOSPITAL', lat: 10.7562, lng: 106.6571, address: '201B Nguyễn Chí Thanh', ward: 'Phường 12', district: 'Quận 5', city: 'Hồ Chí Minh', osmId: 'node/3001' }, { id: 'poi-hospital-002', name: 'Bệnh viện Đại học Y Dược', type: 'HOSPITAL', lat: 10.7569, lng: 106.6641, address: '215 Hồng Bàng', ward: 'Phường 11', district: 'Quận 5', city: 'Hồ Chí Minh', osmId: 'node/3002' }, { id: 'poi-hospital-003', name: 'Bệnh viện FV', type: 'HOSPITAL', lat: 10.7230, lng: 106.7170, address: '6 Nguyễn Lương Bằng', ward: 'Tân Phú', district: 'Quận 7', city: 'Hồ Chí Minh', osmId: 'node/3003' }, { id: 'poi-hospital-004', name: 'Vinmec Central Park', type: 'HOSPITAL', lat: 10.7950, lng: 106.7220, address: 'Vinhomes Central Park', ward: 'Phường 22', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/3004' }, { id: 'poi-hospital-005', name: 'Bệnh viện Thủ Đức', type: 'HOSPITAL', lat: 10.8522, lng: 106.7591, address: '29 Phú Châu', ward: 'Tam Phú', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/3005' }, // ── Clinics ── { id: 'poi-clinic-001', name: 'Phòng khám Đa khoa Quốc tế', type: 'CLINIC', lat: 10.7740, lng: 106.7005, address: '1 Phạm Ngọc Thạch', ward: 'Bến Nghé', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/3101' }, { id: 'poi-clinic-002', name: 'Phòng khám Family Medical Practice', type: 'CLINIC', lat: 10.7841, lng: 106.7015, address: '34 Lê Duẩn', ward: 'Bến Nghé', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/3102' }, // ── Metro Stations (Line 1 - Bến Thành - Suối Tiên) ── { id: 'poi-metro-001', name: 'Ga Bến Thành (Metro Line 1)', type: 'METRO_STATION', lat: 10.7721, lng: 106.6980, address: 'Chợ Bến Thành', ward: 'Bến Thành', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/4001' }, { id: 'poi-metro-002', name: 'Ga Nhà hát Thành phố', type: 'METRO_STATION', lat: 10.7766, lng: 106.7032, address: 'Đường Đồng Khởi', ward: 'Bến Nghé', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/4002' }, { id: 'poi-metro-003', name: 'Ga Ba Son', type: 'METRO_STATION', lat: 10.7862, lng: 106.7130, address: 'Nguyễn Hữu Cảnh', ward: 'Phường 22', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/4003' }, { id: 'poi-metro-004', name: 'Ga Văn Thánh', type: 'METRO_STATION', lat: 10.7945, lng: 106.7188, address: 'Điện Biên Phủ', ward: 'Phường 22', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/4004' }, { id: 'poi-metro-005', name: 'Ga Tân Cảng', type: 'METRO_STATION', lat: 10.7990, lng: 106.7265, address: 'Nguyễn Hữu Cảnh', ward: 'Phường 25', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/4005' }, { id: 'poi-metro-006', name: 'Ga Thảo Điền', type: 'METRO_STATION', lat: 10.8025, lng: 106.7380, address: 'Xa lộ Hà Nội', ward: 'Thảo Điền', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4006' }, { id: 'poi-metro-007', name: 'Ga An Phú', type: 'METRO_STATION', lat: 10.7930, lng: 106.7465, address: 'Xa lộ Hà Nội', ward: 'An Phú', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4007' }, { id: 'poi-metro-008', name: 'Ga Rạch Chiếc', type: 'METRO_STATION', lat: 10.8090, lng: 106.7600, address: 'Xa lộ Hà Nội', ward: 'Phước Long A', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4008' }, { id: 'poi-metro-009', name: 'Ga Phước Long', type: 'METRO_STATION', lat: 10.8165, lng: 106.7705, address: 'Xa lộ Hà Nội', ward: 'Phước Long B', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4009' }, { id: 'poi-metro-010', name: 'Ga Bình Thái', type: 'METRO_STATION', lat: 10.8305, lng: 106.7845, address: 'Xa lộ Hà Nội', ward: 'Bình Thọ', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4010' }, { id: 'poi-metro-011', name: 'Ga Thủ Đức', type: 'METRO_STATION', lat: 10.8420, lng: 106.7935, address: 'Xa lộ Hà Nội', ward: 'Trường Thọ', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4011' }, { id: 'poi-metro-012', name: 'Ga HT Depot', type: 'METRO_STATION', lat: 10.8510, lng: 106.8010, address: 'Xa lộ Hà Nội', ward: 'Hiệp Phú', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4012' }, { id: 'poi-metro-013', name: 'Ga Suối Tiên', type: 'METRO_STATION', lat: 10.8610, lng: 106.8120, address: 'Xa lộ Hà Nội', ward: 'Tân Phú', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4013' }, { id: 'poi-metro-014', name: 'Bến xe Suối Tiên', type: 'METRO_STATION', lat: 10.8680, lng: 106.8210, address: 'Xa lộ Hà Nội', ward: 'Tân Phú', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/4014' }, // ── Malls ── { id: 'poi-mall-001', name: 'Vincom Center Đồng Khởi', type: 'MALL', lat: 10.7778, lng: 106.7021, address: '72 Lê Thánh Tôn', ward: 'Bến Nghé', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/5001' }, { id: 'poi-mall-002', name: 'Takashimaya Saigon Centre', type: 'MALL', lat: 10.7729, lng: 106.6997, address: '65 Lê Lợi', ward: 'Bến Thành', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/5002' }, { id: 'poi-mall-003', name: 'Crescent Mall', type: 'MALL', lat: 10.7295, lng: 106.7205, address: '101 Tôn Dật Tiên', ward: 'Tân Phú', district: 'Quận 7', city: 'Hồ Chí Minh', osmId: 'node/5003' }, { id: 'poi-mall-004', name: 'Vincom Mega Mall Thảo Điền', type: 'MALL', lat: 10.8045, lng: 106.7410, address: '159 Xa lộ Hà Nội', ward: 'Thảo Điền', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/5004' }, { id: 'poi-mall-005', name: 'AEON Mall Bình Tân', type: 'MALL', lat: 10.7423, lng: 106.6092, address: '1 Đường số 17A', ward: 'Bình Trị Đông B', district: 'Bình Tân', city: 'Hồ Chí Minh', osmId: 'node/5005' }, { id: 'poi-mall-006', name: 'Gigamall Thủ Đức', type: 'MALL', lat: 10.8232, lng: 106.7532, address: '240 Phạm Văn Đồng', ward: 'Hiệp Bình Chánh', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/5006' }, // ── Markets ── { id: 'poi-market-001', name: 'Chợ Bến Thành', type: 'MARKET', lat: 10.7722, lng: 106.6980, address: 'Lê Lợi', ward: 'Bến Thành', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/5101' }, { id: 'poi-market-002', name: 'Chợ Bà Chiểu', type: 'MARKET', lat: 10.7996, lng: 106.6953, address: '1 Bạch Đằng', ward: 'Phường 1', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/5102' }, { id: 'poi-market-003', name: 'Chợ Tân Định', type: 'MARKET', lat: 10.7900, lng: 106.6920, address: '48 Mã Lò', ward: 'Tân Định', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/5103' }, // ── Parks ── { id: 'poi-park-001', name: 'Công viên 23/9', type: 'PARK', lat: 10.7700, lng: 106.6930, address: 'Phạm Ngũ Lão', ward: 'Phạm Ngũ Lão', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/6001' }, { id: 'poi-park-002', name: 'Công viên Tao Đàn', type: 'PARK', lat: 10.7760, lng: 106.6912, address: 'Nguyễn Thị Minh Khai', ward: 'Bến Thành', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/6002' }, { id: 'poi-park-003', name: 'Thảo Cầm Viên Sài Gòn', type: 'PARK', lat: 10.7875, lng: 106.7050, address: '2 Nguyễn Bỉnh Khiêm', ward: 'Bến Nghé', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/6003' }, { id: 'poi-park-004', name: 'Công viên Gia Định', type: 'PARK', lat: 10.8135, lng: 106.6795, address: 'Hoàng Minh Giám', ward: 'Phường 9', district: 'Phú Nhuận', city: 'Hồ Chí Minh', osmId: 'node/6004' }, { id: 'poi-park-005', name: 'Công viên Vinhomes Central Park', type: 'PARK', lat: 10.7938, lng: 106.7225, address: 'Ven sông Sài Gòn', ward: 'Phường 22', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/6005' }, { id: 'poi-park-006', name: 'Công viên Gia Long (Quận 7)', type: 'PARK', lat: 10.7335, lng: 106.7250, address: 'Phú Mỹ Hưng', ward: 'Tân Phong', district: 'Quận 7', city: 'Hồ Chí Minh', osmId: 'node/6006' }, // ── Police Stations ── { id: 'poi-police-001', name: 'Công an Quận 1', type: 'POLICE_STATION', lat: 10.7754, lng: 106.6993, address: '123 Lê Duẩn', ward: 'Bến Nghé', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/7001' }, { id: 'poi-police-002', name: 'Công an Quận 7', type: 'POLICE_STATION', lat: 10.7350, lng: 106.7210, address: 'Nguyễn Thị Thập', ward: 'Tân Phú', district: 'Quận 7', city: 'Hồ Chí Minh', osmId: 'node/7002' }, { id: 'poi-police-003', name: 'Công an TP Thủ Đức', type: 'POLICE_STATION', lat: 10.8410, lng: 106.7600, address: 'Võ Văn Ngân', ward: 'Bình Thọ', district: 'Thủ Đức', city: 'Hồ Chí Minh', osmId: 'node/7003' }, { id: 'poi-police-004', name: 'Công an Quận Bình Thạnh', type: 'POLICE_STATION', lat: 10.8040, lng: 106.7010, address: 'Xô Viết Nghệ Tĩnh', ward: 'Phường 21', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/7004' }, // ── Supermarkets ── { id: 'poi-super-001', name: 'Co.opmart Cống Quỳnh', type: 'SUPERMARKET', lat: 10.7680, lng: 106.6900, address: '189C Cống Quỳnh', ward: 'Nguyễn Cư Trinh', district: 'Quận 1', city: 'Hồ Chí Minh', osmId: 'node/8001' }, { id: 'poi-super-002', name: 'VinMart Landmark 81', type: 'SUPERMARKET', lat: 10.7945, lng: 106.7212, address: 'Landmark 81', ward: 'Phường 22', district: 'Bình Thạnh', city: 'Hồ Chí Minh', osmId: 'node/8002' }, { id: 'poi-super-003', name: 'Lotte Mart Quận 7', type: 'SUPERMARKET', lat: 10.7380, lng: 106.7275, address: 'Nguyễn Hữu Thọ', ward: 'Tân Hưng', district: 'Quận 7', city: 'Hồ Chí Minh', osmId: 'node/8003' }, ]; /** * Export function for use in main seed.ts */ export async function seedPOIs(prismaInstance?: PrismaClient) { console.log('📍 Seeding Points of Interest (HCMC)...'); let prisma: PrismaClient; let pool: pg.Pool | null = null; if (prismaInstance) { prisma = prismaInstance; } else { pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] }); const adapter = new PrismaPg(pool); prisma = new PrismaClient({ adapter }); } try { for (const poi of HCMC_POIS) { await prisma.$executeRaw` INSERT INTO "POI" ( "id", "name", "type", "location", "address", "ward", "district", "city", "osmId", "metadata", "createdAt", "updatedAt" ) VALUES ( ${poi.id}, ${poi.name}, ${poi.type}::"POIType", ST_SetSRID(ST_MakePoint(${poi.lng}, ${poi.lat}), 4326), ${poi.address ?? null}, ${poi.ward ?? null}, ${poi.district}, ${poi.city}, ${poi.osmId ?? null}, ${null}::jsonb, NOW(), NOW() ) ON CONFLICT ("id") DO NOTHING `; } console.log(` ✓ ${HCMC_POIS.length} POIs seeded`); // Compute initial NeighborhoodScores for HCMC districts await seedNeighborhoodScores(prisma); } finally { if (!prismaInstance && pool) { await prisma.$disconnect(); await pool.end(); } } } /** * Pre-computed neighborhood scores for key HCMC districts. * In production, the NeighborhoodScoreService will recompute these * using PostGIS ST_DWithin queries against the POI table. */ async function seedNeighborhoodScores(prisma: PrismaClient) { console.log('📊 Seeding neighborhood scores (HCMC)...'); const scores = [ { id: 'ns-q1', district: 'Quận 1', city: 'Hồ Chí Minh', educationScore: 8.5, healthcareScore: 7.5, transportScore: 9.5, shoppingScore: 9.8, greeneryScore: 7.0, safetyScore: 8.5, totalScore: 85.2, poiCounts: { education: 4, healthcare: 3, transport: 5, shopping: 6, park: 3, safety: 2 } }, { id: 'ns-q3', district: 'Quận 3', city: 'Hồ Chí Minh', educationScore: 9.0, healthcareScore: 7.0, transportScore: 7.0, shoppingScore: 8.0, greeneryScore: 6.5, safetyScore: 8.0, totalScore: 77.0, poiCounts: { education: 5, healthcare: 2, transport: 3, shopping: 4, park: 2, safety: 2 } }, { id: 'ns-q5', district: 'Quận 5', city: 'Hồ Chí Minh', educationScore: 7.0, healthcareScore: 9.5, transportScore: 6.5, shoppingScore: 7.5, greeneryScore: 5.0, safetyScore: 7.5, totalScore: 72.0, poiCounts: { education: 3, healthcare: 5, transport: 2, shopping: 3, park: 1, safety: 2 } }, { id: 'ns-q7', district: 'Quận 7', city: 'Hồ Chí Minh', educationScore: 8.0, healthcareScore: 8.0, transportScore: 6.0, shoppingScore: 8.5, greeneryScore: 7.5, safetyScore: 8.5, totalScore: 78.0, poiCounts: { education: 3, healthcare: 3, transport: 2, shopping: 4, park: 2, safety: 2 } }, { id: 'ns-bt', district: 'Bình Thạnh', city: 'Hồ Chí Minh', educationScore: 7.5, healthcareScore: 8.0, transportScore: 8.5, shoppingScore: 7.0, greeneryScore: 8.0, safetyScore: 7.5, totalScore: 77.5, poiCounts: { education: 3, healthcare: 3, transport: 4, shopping: 3, park: 2, safety: 2 } }, { id: 'ns-td', district: 'Thủ Đức', city: 'Hồ Chí Minh', educationScore: 7.0, healthcareScore: 6.5, transportScore: 8.0, shoppingScore: 7.0, greeneryScore: 6.0, safetyScore: 7.0, totalScore: 70.0, poiCounts: { education: 3, healthcare: 2, transport: 8, shopping: 3, park: 1, safety: 2 } }, { id: 'ns-pn', district: 'Phú Nhuận', city: 'Hồ Chí Minh', educationScore: 7.5, healthcareScore: 6.5, transportScore: 7.5, shoppingScore: 7.5, greeneryScore: 7.5, safetyScore: 8.0, totalScore: 75.0, poiCounts: { education: 2, healthcare: 2, transport: 3, shopping: 3, park: 2, safety: 1 } }, { id: 'ns-gv', district: 'Gò Vấp', city: 'Hồ Chí Minh', educationScore: 6.5, healthcareScore: 6.0, transportScore: 6.0, shoppingScore: 6.5, greeneryScore: 5.5, safetyScore: 7.0, totalScore: 63.0, poiCounts: { education: 2, healthcare: 2, transport: 2, shopping: 2, park: 1, safety: 1 } }, { id: 'ns-btan', district: 'Bình Tân', city: 'Hồ Chí Minh', educationScore: 5.5, healthcareScore: 5.0, transportScore: 5.0, shoppingScore: 6.5, greeneryScore: 4.5, safetyScore: 6.0, totalScore: 55.0, poiCounts: { education: 1, healthcare: 1, transport: 1, shopping: 2, park: 1, safety: 1 } }, { id: 'ns-q10', district: 'Quận 10', city: 'Hồ Chí Minh', educationScore: 8.5, healthcareScore: 7.0, transportScore: 7.0, shoppingScore: 7.5, greeneryScore: 5.5, safetyScore: 7.5, totalScore: 72.0, poiCounts: { education: 3, healthcare: 2, transport: 3, shopping: 3, park: 1, safety: 2 } }, ]; for (const s of scores) { await prisma.$executeRaw` INSERT INTO "NeighborhoodScore" ( "id", "district", "city", "educationScore", "healthcareScore", "transportScore", "shoppingScore", "greeneryScore", "safetyScore", "totalScore", "poiCounts", "calculatedAt", "createdAt", "updatedAt" ) VALUES ( ${s.id}, ${s.district}, ${s.city}, ${s.educationScore}, ${s.healthcareScore}, ${s.transportScore}, ${s.shoppingScore}, ${s.greeneryScore}, ${s.safetyScore}, ${s.totalScore}, ${JSON.stringify(s.poiCounts)}::jsonb, NOW(), NOW(), NOW() ) ON CONFLICT ("district", "city") DO UPDATE SET "educationScore" = EXCLUDED."educationScore", "healthcareScore" = EXCLUDED."healthcareScore", "transportScore" = EXCLUDED."transportScore", "shoppingScore" = EXCLUDED."shoppingScore", "greeneryScore" = EXCLUDED."greeneryScore", "safetyScore" = EXCLUDED."safetyScore", "totalScore" = EXCLUDED."totalScore", "poiCounts" = EXCLUDED."poiCounts", "calculatedAt" = NOW(), "updatedAt" = NOW() `; } console.log(` ✓ ${scores.length} neighborhood scores seeded`); } // Standalone execution if (require.main === module) { seedPOIs() .then(() => { console.log('✅ POI seed completed'); process.exit(0); }) .catch((e) => { console.error('❌ POI seed failed:', e); process.exit(1); }); }