-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstore_scraper.js
More file actions
144 lines (116 loc) · 4.23 KB
/
Copy pathstore_scraper.js
File metadata and controls
144 lines (116 loc) · 4.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import "dotenv/config";
import { createClient } from "@supabase/supabase-js";
import { fetchFromOverpass, parseStore } from "./scrape_area.js";
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY
);
// ─── Distance helper ───
function distanceMiles(lat1, lng1, lat2, lng2) {
const dLat = (lat2 - lat1) * 69;
const dLng = (lng2 - lng1) * 69 * Math.cos(((lat1 + lat2) / 2) * Math.PI / 180);
return Math.sqrt(dLat * dLat + dLng * dLng);
}
// ─── Clustering ───
// Groups nearby user locations so overlapping areas aren't scraped twice.
// Each cluster uses the largest radius among its members.
function clusterLocations(locations) {
const clusters = [];
for (const loc of locations) {
const radius = loc.radius || 20;
const alreadyCovered = clusters.some(
(c) => distanceMiles(c.lat, c.lng, loc.lat, loc.lng) <= Math.max(c.radius, radius)
);
if (!alreadyCovered) {
clusters.push({ lat: loc.lat, lng: loc.lng, radius });
}
}
return clusters;
}
// ─── Bounding box from center + radius ───
function buildBoundingBox(lat, lng, radiusMiles) {
const latDelta = radiusMiles / 69;
const lngDelta = radiusMiles / (69 * Math.cos(lat * Math.PI / 180));
return {
south: lat - latDelta,
west: lng - lngDelta,
north: lat + latDelta,
east: lng + lngDelta,
};
}
// ─── Upsert stores into the database ───
async function upsertStores(stores) {
if (stores.length === 0) return 0;
const chunkSize = 100;
let total = 0;
for (let i = 0; i < stores.length; i += chunkSize) {
const chunk = stores.slice(i, i + chunkSize);
const { error } = await supabase
.from("stores")
.upsert(chunk, { onConflict: "osm_id" });
if (error) {
console.error(` Error upserting chunk at index ${i}:`, error.message);
} else {
total += chunk.length;
}
}
return total;
}
// ─── Main ───
async function runScraper() {
console.log(`[store_scraper] Starting at ${new Date().toISOString()}`);
// 1. Fetch all active user locations with their preferred radius
const { data: userLocations, error } = await supabase
.from("user_locations")
.select("lat, lng, radius_miles");
if (error) {
console.error("[store_scraper] Failed to fetch user locations:", error.message);
process.exit(1);
}
if (!userLocations || userLocations.length === 0) {
console.log("[store_scraper] No user locations found. Nothing to scrape.");
return;
}
console.log(`[store_scraper] Found ${userLocations.length} user location(s)`);
// 2. Cluster overlapping locations using each user's radius
const clusters = clusterLocations(
userLocations.map((l) => ({
lat: parseFloat(l.lat),
lng: parseFloat(l.lng),
radius: l.radius_miles || 20,
}))
);
console.log(`[store_scraper] Clustered into ${clusters.length} unique scrape zone(s)`);
// 3. Scrape each cluster
let totalStores = 0;
for (let i = 0; i < clusters.length; i++) {
const cluster = clusters[i];
const bbox = buildBoundingBox(cluster.lat, cluster.lng, cluster.radius);
console.log(
`\n[store_scraper] Zone ${i + 1}/${clusters.length}: ` +
`center (${cluster.lat.toFixed(4)}, ${cluster.lng.toFixed(4)}), ` +
`radius ${cluster.radius} mi`
);
try {
const elements = await fetchFromOverpass(bbox.south, bbox.west, bbox.north, bbox.east);
console.log(` Found ${elements.length} raw results from Overpass`);
const stores = elements.map(parseStore).filter(Boolean);
console.log(` Parsed ${stores.length} valid stores`);
const upserted = await upsertStores(stores);
console.log(` Upserted ${upserted} stores`);
totalStores += upserted;
// Be respectful to the free Overpass API
if (i < clusters.length - 1) {
console.log(" Waiting 3s before next zone...");
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} catch (err) {
console.error(` Error scraping zone ${i + 1}:`, err.message);
}
}
console.log(`\n[store_scraper] Complete. Total stores upserted: ${totalStores}`);
}
runScraper().catch((err) => {
console.error("[store_scraper] Fatal error:", err);
process.exit(1);
});