From 31970b86fcb8f826658fb93e7ee818b1b94daef3 Mon Sep 17 00:00:00 2001 From: devthejo Date: Thu, 5 Mar 2026 16:30:12 +0100 Subject: [PATCH] feat(dae): first draft --- .claude/settings.local.json | 12 + .gitignore | 12 + metro.config.js | 2 +- scripts/.yarnrc.yml | 7 + scripts/csv-to-sqlite.mjs | 205 +++++++++ scripts/lib/schema.sql | 15 + scripts/package.json | 16 + scripts/yarn.lock | 889 ++++++++++++++++++++++++++++++++++++ src/data/getNearbyDefibs.js | 65 +++ src/db/defibsRepo.js | 177 +++++++ src/db/openDb.js | 41 ++ src/db/openDb.op-sqlite.js | 17 + src/utils/geo/haversine.js | 14 + 13 files changed, 1471 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.local.json create mode 100644 scripts/.yarnrc.yml create mode 100644 scripts/csv-to-sqlite.mjs create mode 100644 scripts/lib/schema.sql create mode 100644 scripts/package.json create mode 100644 scripts/yarn.lock create mode 100644 src/data/getNearbyDefibs.js create mode 100644 src/db/defibsRepo.js create mode 100644 src/db/openDb.js create mode 100644 src/db/openDb.op-sqlite.js create mode 100644 src/utils/geo/haversine.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e221c41 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(cat /home/jo/lab/alerte-secours/apps/as-app/src/biz/*.js)", + "Bash(npm install)", + "Bash(node csv-to-sqlite.mjs --input ../.data/geodae.csv --output ../src/assets/db/geodae.db)", + "Bash(sqlite3 /home/jo/lab/alerte-secours/apps/as-app/src/assets/db/geodae.db \"SELECT count\\(*\\) FROM defibs; SELECT * FROM defibs LIMIT 3; SELECT count\\(DISTINCT h3\\) FROM defibs;\")", + "Bash(sqlite3 /home/jo/lab/alerte-secours/apps/as-app/src/assets/db/geodae.db \"SELECT id, nom, latitude, longitude FROM defibs WHERE h3 = ''881fb542d3fffff'' LIMIT 5;\")", + "Bash(sqlite3 /home/jo/lab/alerte-secours/apps/as-app/src/assets/db/geodae.db \"SELECT h3, count\\(*\\) as cnt FROM defibs GROUP BY h3 ORDER BY cnt DESC LIMIT 5;\")" + ] + } +} diff --git a/.gitignore b/.gitignore index dfc7db2..a041857 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,15 @@ android/app/google-services.json !android/app/google-services.example.json screenshot-*.png + +/.data + +# Geodae preprocessing +scripts/node_modules/ +scripts/.yarn/* +!scripts/.yarn/patches +!scripts/.yarn/plugins +!scripts/.yarn/releases +!scripts/.yarn/sdks +!scripts/.yarn/versions +src/assets/db/*.db diff --git a/metro.config.js b/metro.config.js index 341a168..93313e4 100644 --- a/metro.config.js +++ b/metro.config.js @@ -15,7 +15,7 @@ const config = { /node_modules\/.*\/android\/build\/intermediates\/(library_jni|merged_jni_libs)\/.*/, ]), sourceExts: [...sentryConfig.resolver.sourceExts, "cjs"], - assetExts: [...defaultConfig.resolver.assetExts, "ttf"], + assetExts: [...defaultConfig.resolver.assetExts, "ttf", "db"], }, server: { enhanceMiddleware: (middleware) => { diff --git a/scripts/.yarnrc.yml b/scripts/.yarnrc.yml new file mode 100644 index 0000000..986aa42 --- /dev/null +++ b/scripts/.yarnrc.yml @@ -0,0 +1,7 @@ +compressionLevel: mixed + +enableGlobalCache: false + +nodeLinker: node-modules + +yarnPath: ../.yarn/releases/yarn-4.5.3.cjs diff --git a/scripts/csv-to-sqlite.mjs b/scripts/csv-to-sqlite.mjs new file mode 100644 index 0000000..12365de --- /dev/null +++ b/scripts/csv-to-sqlite.mjs @@ -0,0 +1,205 @@ +#!/usr/bin/env node +// CSV-to-SQLite pipeline for defibrillator data with H3 geo-indexing. +// Usage: node csv-to-sqlite.mjs --input --output [--h3res 8] [--delimiter auto] [--batchSize 5000] + +import { createReadStream, readFileSync, existsSync, unlinkSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { parseArgs } from "node:util"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createRequire } from "node:module"; +import { parse } from "csv-parse"; + +const require = createRequire(import.meta.url); +const Database = require("better-sqlite3"); +const h3 = require("h3-js"); + +// --------------------------------------------------------------------------- +// CLI args +// --------------------------------------------------------------------------- + +const { values: args } = parseArgs({ + options: { + input: { type: "string", short: "i" }, + output: { type: "string", short: "o" }, + h3res: { type: "string", default: "8" }, + delimiter: { type: "string", default: "auto" }, + batchSize: { type: "string", default: "5000" }, + }, +}); + +const INPUT = args.input; +const OUTPUT = args.output; +const H3_RES = parseInt(args.h3res, 10); +const BATCH_SIZE = parseInt(args.batchSize, 10); + +if (!INPUT || !OUTPUT) { + console.error("Usage: node csv-to-sqlite.mjs --input --output [--h3res 8] [--delimiter auto] [--batchSize 5000]"); + process.exit(1); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCHEMA_PATH = resolve(__dirname, "lib", "schema.sql"); + +function detectDelimiter(filePath) { + // Read first line to detect delimiter + const chunk = readFileSync(filePath, { encoding: "utf-8", end: 4096 }); + const firstLine = chunk.split(/\r?\n/)[0]; + const commaCount = (firstLine.match(/,/g) || []).length; + const semicolonCount = (firstLine.match(/;/g) || []).length; + const detected = semicolonCount > commaCount ? ";" : ","; + console.log(`Delimiter auto-detected: "${detected}" (commas=${commaCount}, semicolons=${semicolonCount})`); + return detected; +} + +function computeH3(lat, lon, res) { + return h3.latLngToCell(lat, lon, res); +} + +function deterministicId(lat, lon, nom, adresse) { + const payload = `${lat}|${lon}|${nom}|${adresse}`; + return createHash("sha256").update(payload, "utf-8").digest("hex").slice(0, 16); +} + +function cleanFloat(val) { + const n = parseFloat(val); + return Number.isFinite(n) ? n : null; +} + +function cleanInt(val) { + const n = parseInt(val, 10); + return Number.isFinite(n) ? n : 0; +} + +function cleanStr(val) { + return (val ?? "").trim(); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + const delimiter = + args.delimiter === "auto" ? detectDelimiter(INPUT) : args.delimiter; + + // Remove existing output to avoid UNIQUE constraint errors on re-run + if (existsSync(OUTPUT)) { + unlinkSync(OUTPUT); + console.log(`Removed existing DB: ${OUTPUT}`); + } + + // Open database with fast-import PRAGMAs + const db = new Database(OUTPUT); + db.pragma("journal_mode = OFF"); + db.pragma("synchronous = OFF"); + db.pragma("temp_store = MEMORY"); + db.pragma("cache_size = -64000"); // 64 MB + db.pragma("locking_mode = EXCLUSIVE"); + + // Create schema + const schema = readFileSync(SCHEMA_PATH, "utf-8"); + db.exec(schema); + + // Prepare insert statement + const insert = db.prepare( + `INSERT OR IGNORE INTO defibs (id, latitude, longitude, nom, adresse, horaires, acces, disponible_24h, h3) + VALUES (@id, @latitude, @longitude, @nom, @adresse, @horaires, @acces, @disponible_24h, @h3)` + ); + + const insertMany = db.transaction((rows) => { + for (const row of rows) { + insert.run(row); + } + }); + + // Streaming CSV parse + const parser = createReadStream(INPUT, { encoding: "utf-8" }).pipe( + parse({ + delimiter, + columns: true, + skip_empty_lines: true, + trim: true, + relax_column_count: true, + bom: true, + quote: '"', + escape: '"', + }) + ); + + let batch = []; + let total = 0; + let skipped = 0; + + for await (const record of parser) { + const lat = cleanFloat(record.latitude); + const lon = cleanFloat(record.longitude); + + // Skip rows with invalid coordinates + if (lat === null || lon === null || lat < -90 || lat > 90 || lon < -180 || lon > 180) { + skipped++; + continue; + } + + const nom = cleanStr(record.nom); + const adresse = cleanStr(record.adresse); + const horaires = cleanStr(record.horaires); + const acces = cleanStr(record.acces); + const disponible_24h = cleanInt(record.disponible_24h); + const id = deterministicId(lat, lon, nom, adresse); + const h3Cell = computeH3(lat, lon, H3_RES); + + batch.push({ + id, + latitude: lat, + longitude: lon, + nom, + adresse, + horaires, + acces, + disponible_24h, + h3: h3Cell, + }); + + if (batch.length >= BATCH_SIZE) { + insertMany(batch); + total += batch.length; + batch = []; + process.stdout.write(`\rInserted ${total} rows...`); + } + } + + // Flush remaining + if (batch.length > 0) { + insertMany(batch); + total += batch.length; + } + + console.log(`\nDone: ${total} rows inserted, ${skipped} skipped.`); + + // Restore safe PRAGMAs for the shipped DB + db.pragma("journal_mode = DELETE"); + db.pragma("synchronous = NORMAL"); + + // VACUUM to compact + db.exec("VACUUM"); + + // Final stats + const count = db.prepare("SELECT count(*) AS cnt FROM defibs").get(); + const pageCount = db.pragma("page_count", { simple: true }); + const pageSize = db.pragma("page_size", { simple: true }); + const sizeBytes = pageCount * pageSize; + console.log(`DB rows: ${count.cnt}`); + console.log(`DB size: ${(sizeBytes / 1024 / 1024).toFixed(2)} MB`); + + db.close(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/lib/schema.sql b/scripts/lib/schema.sql new file mode 100644 index 0000000..d4ea556 --- /dev/null +++ b/scripts/lib/schema.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS defibs ( + id TEXT PRIMARY KEY NOT NULL, + latitude REAL NOT NULL, + longitude REAL NOT NULL, + nom TEXT NOT NULL DEFAULT '', + adresse TEXT NOT NULL DEFAULT '', + horaires TEXT NOT NULL DEFAULT '', + acces TEXT NOT NULL DEFAULT '', + disponible_24h INTEGER NOT NULL DEFAULT 0, + h3 TEXT NOT NULL DEFAULT '' +); + +CREATE INDEX IF NOT EXISTS idx_defibs_h3 ON defibs (h3); +CREATE INDEX IF NOT EXISTS idx_defibs_latlon ON defibs (latitude, longitude); +CREATE INDEX IF NOT EXISTS idx_defibs_dispo ON defibs (disponible_24h); diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..50d2e5f --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,16 @@ +{ + "name": "geodae-pipeline", + "version": "1.0.0", + "private": true, + "type": "module", + "packageManager": "yarn@4.5.3", + "scripts": { + "build-db": "node csv-to-sqlite.mjs --input ../.data/geodae.csv --output ../src/assets/db/geodae.db", + "build-db:semicolon": "node csv-to-sqlite.mjs --input ../.data/geodae.csv --output ../src/assets/db/geodae.db --delimiter ';'" + }, + "dependencies": { + "better-sqlite3": "^11.7.0", + "csv-parse": "^5.6.0", + "h3-js": "^4.2.1" + } +} diff --git a/scripts/yarn.lock b/scripts/yarn.lock new file mode 100644 index 0000000..086ee0e --- /dev/null +++ b/scripts/yarn.lock @@ -0,0 +1,889 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10 + +"@gar/promise-retry@npm:^1.0.0": + version: 1.0.2 + resolution: "@gar/promise-retry@npm:1.0.2" + dependencies: + retry: "npm:^0.13.1" + checksum: 10/b91326999ce94677cbe91973079eabc689761a93a045f6a2d34d4070e9305b27f6c54e4021688c7080cb14caf89eafa0c0f300af741b94c20d18608bdb66ca46 + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10/4412e9e6713c89c1e66d80bb0bb5a2a93192f10477623a27d08f228ba0316bb880affabc5bfe7f838f58a34d26c2c190da726e576cdfc18c49a72e89adabdcf5 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^11.2.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10/1a81573becc60515031accc696e6405e9b894e65c12b98ef4aeee03b5617c41948633159dbf6caf5dde5b47367eeb749bdc7b7dfb21960930a9060a935c6f636 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/4935c7719d17830d0f9fa46c50be17b2a3c945cec61760f6d0909bce47677c42e1810ca673305890f9e84f008ec4d8e841182f371e42100a8159d15f22249208 + languageName: node + linkType: hard + +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: 10/e2f0c6a6708ad738b3e8f50233f4800de31ad41a6cdc50e0cbe51b76fed69fd0213516d92c15ce1a9985fca71a14606a9be22bf00f8475a58987b9bfb671c582 + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 10/79bef167247789f955aaba113bae74bf64aa1e1acca4b1d6bb444bdf91d82c3e07e9451ef6a6e2e35e8f71a6f97ce33e3d855a5328eb9fad1bc3cc4cfd031ed8 + languageName: node + linkType: hard + +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + +"better-sqlite3@npm:^11.7.0": + version: 11.10.0 + resolution: "better-sqlite3@npm:11.10.0" + dependencies: + bindings: "npm:^1.5.0" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.1.1" + checksum: 10/5e4c7437c4fe6033335a79c82974d7ab29f33c51c36f48b73e87e087d21578468575de1c56a7badd4f76f17255e25abefddaeacf018e5eeb9e0cb8d6e3e4a5e1 + languageName: node + linkType: hard + +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 10/593d5ae975ffba15fbbb4788fe5abd1e125afbab849ab967ab43691d27d6483751805d98cb92f7ac24a2439a8a8678cd0131c535d5d63de84e383b0ce2786133 + languageName: node + linkType: hard + +"bl@npm:^4.0.3": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10/b7904e66ed0bdfc813c06ea6c3e35eafecb104369dbf5356d0f416af90c1546de3b74e5b63506f0629acf5e16a6f87c3798f16233dcff086e9129383aa02ab55 + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.2": + version: 5.0.4 + resolution: "brace-expansion@npm:5.0.4" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/cfd57e20d8ded9578149e47ae4d3fff2b2f78d06b54a32a73057bddff65c8e9b930613f0cbcfefedf12dd117151e19d4da16367d5127c54f3bff02d8a4479bb2 + languageName: node + linkType: hard + +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10/997434d3c6e3b39e0be479a80288875f71cd1c07d75a3855e6f08ef848a3c966023f79534e22e415ff3a5112708ce06127277ab20e527146d55c84566405c7c6 + languageName: node + linkType: hard + +"cacache@npm:^20.0.1": + version: 20.0.3 + resolution: "cacache@npm:20.0.3" + dependencies: + "@npmcli/fs": "npm:^5.0.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^13.0.0" + lru-cache: "npm:^11.1.0" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^7.0.2" + ssri: "npm:^13.0.0" + unique-filename: "npm:^5.0.0" + checksum: 10/388a0169970df9d051da30437f93f81b7e91efb570ad0ff2b8fde33279fbe726c1bc8e8e2b9c05053ffb4f563854c73db395e8712e3b62347a1bc4f7fb8899ff + languageName: node + linkType: hard + +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10/b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c + languageName: node + linkType: hard + +"csv-parse@npm:^5.6.0": + version: 5.6.0 + resolution: "csv-parse@npm:5.6.0" + checksum: 10/4c82e11f50ae0ccbac2aed716ef2502d0468bf96552083561db789fc0258ee4bb0a30106fcfb2684f153cb4042f0413e0eac3645d5466874803b7ccdeba67ac8 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad + languageName: node + linkType: hard + +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10/d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812 + languageName: node + linkType: hard + +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 10/7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7 + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.0": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766 + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: "npm:^1.4.0" + checksum: 10/1e0cfa6e7f49887544e03314f9dfc56a8cb6dde910cbb445983ecc2ff426fc05946df9d75d8a21a3a64f2cecfe1bf88f773952029f46756b2ed64a24e95b1fb8 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 10/588c19847216421ed92befb521767b7018dc88f88b0576df98cb242f20961425e96a92cbece525ef28cc5becceae5d544ae0f5b9b5e2aa05acb13716ca5b3099 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 10/ca25962b4bbab943b7c4ed0b5228e263833a5063c65e1cdeac4be9afad350aae5466e8e619b5051f4f8d37b2144a2d6e8fcc771b6cc82934f7dade2f964f652c + languageName: node + linkType: hard + +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: 10/b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/af143246cf6884fe26fa281621d45cfe111d34b30535a475bfa38dafe343dadb466c047a924ffc7d6b7b18265df4110224ce3803806dbb07173bf2087b648d7f + languageName: node + linkType: hard + +"geodae-pipeline@workspace:.": + version: 0.0.0-use.local + resolution: "geodae-pipeline@workspace:." + dependencies: + better-sqlite3: "npm:^11.7.0" + csv-parse: "npm:^5.6.0" + h3-js: "npm:^4.2.1" + languageName: unknown + linkType: soft + +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 10/2a091ba07fbce22205642543b4ea8aaf068397e1433c00ae0f9de36a3607baf5bcc14da97fbb798cfca6393b3c402031fca06d8b491a44206d6efef391c58537 + languageName: node + linkType: hard + +"glob@npm:^13.0.0": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 + languageName: node + linkType: hard + +"h3-js@npm:^4.2.1": + version: 4.4.0 + resolution: "h3-js@npm:4.4.0" + checksum: 10/6db6888f143ed6a1e3ca10506f15c35679afd181e24b71bcdc90259206e3f02637bab38e2a35382d51f17151ea193dfab69c01ff3e31bf0e86abfb1957692576 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10/4efd2dfcfeea9d5e88c84af450b9980be8a43c2c8179508b1c57c7b4421c855f3e8efe92fa53e0b3f4a43c85824ada930eabbc306d1b3beab750b6dcc5187693 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10/d062acfa0cb82beeb558f1043c6ba770ea892b5fb7b28654dbc70ea2aeea55226dd34c02a294f6c1ca179a5aa483c4ea641846821b182edbd9cc5d89b54c6848 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:4" + checksum: 10/784b628cbd55b25542a9d85033bdfd03d4eda630fb8b3c9477959367f3be95dc476ed2ecbb9836c359c7c698027fc7b45723a302324433590f45d6c1706e8c13 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.7.2": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10/24c937b532f868e938386b62410b303b7c767ce3d08dc2829cbe59464d5a26ef86ae5ad1af6b34eec43ddfea39e7d101638644b0178d67262fa87015d59f983a + languageName: node + linkType: hard + +"ieee754@npm:^1.1.13": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10/2d30b157a91fe1c1d7c6f653cbf263f039be6c5bfa959245a16d4ee191fc0f2af86c08545b6e6beeb041c56b574d2d5b9f95343d378ab49c0f37394d541e7fc8 + languageName: node + linkType: hard + +"inherits@npm:^2.0.3, inherits@npm:^2.0.4": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 + languageName: node + linkType: hard + +"ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: 10/314ae176e8d4deb3def56106da8002b462221c174ddb7ce0c49ee72c8cd1f9044f7b10cc555a7d8850982c3b9ca96fc212122749f5234bc2b6fb05fb942ed566 + languageName: node + linkType: hard + +"ip-address@npm:^10.0.1": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 10/a6979629d1ad9c1fb424bc25182203fad739b40225aebc55ec6243bbff5035faf7b9ed6efab3a097de6e713acbbfde944baacfa73e11852bb43989c45a68d79e + languageName: node + linkType: hard + +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 10/2ead327ef596042ef9c9ec5f236b316acfaedb87f4bb61b3c3d574fb2e9c8a04b67305e04733bde52c24d9622fdebd3270aadb632adfbf9cadef88fe30f479e5 + languageName: node + linkType: hard + +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": + version: 11.2.6 + resolution: "lru-cache@npm:11.2.6" + checksum: 10/91222bbd59f793a0a0ad57789388f06b34ac9bb1613433c1d1810457d09db5cd3ec8943227ce2e1f5d6a0a15d6f1a9f129cb2c49ae9b6b10e82d4965fddecbef + languageName: node + linkType: hard + +"make-fetch-happen@npm:^15.0.0": + version: 15.0.4 + resolution: "make-fetch-happen@npm:15.0.4" + dependencies: + "@gar/promise-retry": "npm:^1.0.0" + "@npmcli/agent": "npm:^4.0.0" + cacache: "npm:^20.0.1" + http-cache-semantics: "npm:^4.1.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^5.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^1.0.0" + proc-log: "npm:^6.0.0" + ssri: "npm:^13.0.0" + checksum: 10/4aa75baab500eff4259f2e1a3e76cf01ab3a3cd750037e4bd7b5e22bc5a60f12cc766b3c45e6288accb5ab609e88de5019a8014e0f96f6594b7b03cb504f4b81 + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10/7e719047612411fe071332a7498cf0448bbe43c485c0d780046c76633a771b223ff49bd00267be122cedebb897037fdb527df72335d0d0f74724604ca70b37ad + languageName: node + linkType: hard + +"minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10/aea4874e521c55bb60744685bbffe3d152e5460f84efac3ea936e6bbe2ceba7deb93345fec3f9bb17f7b6946776073a64d40ae32bf5f298ad690308121068a1f + languageName: node + linkType: hard + +"minimist@npm:^1.2.0, minimist@npm:^1.2.3": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 + languageName: node + linkType: hard + +"minipass-fetch@npm:^5.0.0": + version: 5.0.2 + resolution: "minipass-fetch@npm:5.0.2" + dependencies: + iconv-lite: "npm:^0.7.2" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^2.0.0" + minizlib: "npm:^3.0.1" + dependenciesMeta: + iconv-lite: + optional: true + checksum: 10/4f3f65ea5b20a3a287765ebf21cc73e62031f754944272df2a3039296cc75a8fc2dc50b8a3c4f39ce3ac6e5cc583e8dc664d12c6ab98e0883d263e49f344bc86 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^2.0.0": + version: 2.0.0 + resolution: "minipass-sized@npm:2.0.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10/3b89adf64ca705662f77481e278eff5ec0a57aeffb5feba7cc8843722b1e7770efc880f2a17d1d4877b2d7bf227873cd46afb4da44c0fd18088b601ea50f96bb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10/a5c6ef069f70d9a524d3428af39f2b117ff8cd84172e19b754e7264a33df460873e6eb3d6e55758531580970de50ae950c496256bb4ad3691a2974cddff189f0 + languageName: node + linkType: hard + +"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 + languageName: node + linkType: hard + +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + languageName: node + linkType: hard + +"napi-build-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "napi-build-utils@npm:2.0.0" + checksum: 10/69adcdb828481737f1ec64440286013f6479d5b264e24d5439ba795f65293d0bb6d962035de07c65fae525ed7d2fcd0baab6891d8e3734ea792fec43918acf83 + languageName: node + linkType: hard + +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 + languageName: node + linkType: hard + +"node-abi@npm:^3.3.0": + version: 3.87.0 + resolution: "node-abi@npm:3.87.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/3c7beafed49d9486b4bd95166bcf182b26d4aafa63c8620d1b3bd70a740fc256e1789453cfd83653b6efa7d259f0b1a34eaa95a6c62e74974ad99129bc78842f + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^15.0.0" + nopt: "npm:^9.0.0" + proc-log: "npm:^6.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + which: "npm:^6.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10/4ebab5b77585a637315e969c2274b5520562473fe75de850639a580c2599652fb9f33959ec782ea45a2e149d8f04b548030f472eeeb3dbdf19a7f2ccbc30b908 + languageName: node + linkType: hard + +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" + dependencies: + abbrev: "npm:^4.0.0" + bin: + nopt: bin/nopt.js + checksum: 10/56a1ccd2ad711fb5115918e2c96828703cddbe12ba2c3bd00591758f6fa30e6f47dd905c59dbfcf9b773f3a293b45996609fb6789ae29d6bfcc3cf3a6f7d9fda + languageName: node + linkType: hard + +"once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10/cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10/ef48c3b2e488f31c693c9fcc0df0ef76518cf6426a495cf9486ebbb0fd7f31aef7f90e96f72e0070c0ff6e3177c9318f644b512e2c29e3feee8d7153fcb6782e + languageName: node + linkType: hard + +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 + languageName: node + linkType: hard + +"picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10/57b99055f40b16798f2802916d9c17e9744e620a0db136554af01d19598b96e45e2f00014c91d1b8b13874b80caa8c295b3d589a3f72373ec4aaf54baa5962d5 + languageName: node + linkType: hard + +"prebuild-install@npm:^7.1.1": + version: 7.1.3 + resolution: "prebuild-install@npm:7.1.3" + dependencies: + detect-libc: "npm:^2.0.0" + expand-template: "npm:^2.0.3" + github-from-package: "npm:0.0.0" + minimist: "npm:^1.2.3" + mkdirp-classic: "npm:^0.5.3" + napi-build-utils: "npm:^2.0.0" + node-abi: "npm:^3.3.0" + pump: "npm:^3.0.0" + rc: "npm:^1.2.7" + simple-get: "npm:^4.0.0" + tar-fs: "npm:^2.0.0" + tunnel-agent: "npm:^0.6.0" + bin: + prebuild-install: bin.js + checksum: 10/1b7e4c00d2750b532a4fc2a83ffb0c5fefa1b6f2ad071896ead15eeadc3255f5babd816949991af083cf7429e375ae8c7d1c51f73658559da36f948a020a3a11 + languageName: node + linkType: hard + +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.4 + resolution: "pump@npm:3.0.4" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10/d043c3e710c56ffd280711e98a94e863ab334f79ea43cee0fb70e1349b2355ffd2ff287c7522e4c960a247699d5b7825f00fa090b85d6179c973be13f78a6c49 + languageName: node + linkType: hard + +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: "npm:^0.6.0" + ini: "npm:~1.3.0" + minimist: "npm:^1.2.0" + strip-json-comments: "npm:~2.0.1" + bin: + rc: ./cli.js + checksum: 10/5c4d72ae7eec44357171585938c85ce066da8ca79146b5635baf3d55d74584c92575fa4e2c9eac03efbed3b46a0b2e7c30634c012b4b4fa40d654353d3c163eb + languageName: node + linkType: hard + +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 + languageName: node + linkType: hard + +"retry@npm:^0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 10/6125ec2e06d6e47e9201539c887defba4e47f63471db304c59e4b82fc63c8e89ca06a77e9d34939a9a42a76f00774b2f46c0d4a4cbb3e287268bd018ed69426d + languageName: node + linkType: hard + +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 + languageName: node + linkType: hard + +"semver@npm:^7.3.5": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 10/26bdc6d58b29528f4142d29afb8526bc335f4fc04c4a10f2b98b217f277a031c66736bf82d3d3bb354a2f6a3ae50f18fd62b053c4ac3f294a3d10a61f5075b75 + languageName: node + linkType: hard + +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 10/4d211042cc3d73a718c21ac6c4e7d7a0363e184be6a5ad25c8a1502e49df6d0a0253979e3d50dbdd3f60ef6c6c58d756b5d66ac1e05cda9cacd2e9fc59e3876a + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: "npm:^6.0.0" + once: "npm:^1.3.1" + simple-concat: "npm:^1.0.0" + checksum: 10/93f1b32319782f78f2f2234e9ce34891b7ab6b990d19d8afefaa44423f5235ce2676aae42d6743fecac6c8dfff4b808d4c24fe5265be813d04769917a9a44f36 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10/927484aa0b1640fd9473cee3e0a0bcad6fce93fd7bbc18bac9ad0c33686f5d2e2c422fba24b5899c184524af01e11dd2bd051c2bf2b07e47aff8ca72cbfc60d2 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10/ee99e1dacab0985b52cbe5a75640be6e604135e9489ebdc3048635d186012fbaecc20fbbe04b177dee434c319ba20f09b3e7dfefb7d932466c0d707744eac05c + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.7 + resolution: "socks@npm:2.8.7" + dependencies: + ip-address: "npm:^10.0.1" + smart-buffer: "npm:^4.2.0" + checksum: 10/d19366c95908c19db154f329bbe94c2317d315dc933a7c2b5101e73f32a555c84fb199b62174e1490082a593a4933d8d5a9b297bde7d1419c14a11a965f51356 + languageName: node + linkType: hard + +"ssri@npm:^13.0.0": + version: 13.0.1 + resolution: "ssri@npm:13.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/ae560d0378d074006a71b06af71bfbe84a3fe1ac6e16c1f07575f69e670d40170507fe52b21bcc23399429bc6a15f4bc3ea8d9bc88e9dfd7e87de564e6da6a72 + languageName: node + linkType: hard + +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10/54d23f4a6acae0e93f999a585e673be9e561b65cd4cca37714af1e893ab8cd8dfa52a9e4f58f48f87b4a44918d3a9254326cb80ed194bf2e4c226e2b21767e56 + languageName: node + linkType: hard + +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 10/1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 + languageName: node + linkType: hard + +"tar-fs@npm:^2.0.0": + version: 2.1.4 + resolution: "tar-fs@npm:2.1.4" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10/bdf7e3cb039522e39c6dae3084b1bca8d7bcc1de1906eae4a1caea6a2250d22d26dcc234118bf879b345d91ebf250a744b196e379334a4abcbb109a78db7d3be + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a + languageName: node + linkType: hard + +"tar@npm:^7.5.4": + version: 7.5.10 + resolution: "tar@npm:7.5.10" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10/98ba6421a250b233c36a54f7441647bdfee1ed0b916cd57850259a3602154d996f5b8422f67ef5c8ce77f582ed938054775c2873fc7c901e0c7530ed50febc40 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 + languageName: node + linkType: hard + +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10/7f0d9ed5c22404072b2ae8edc45c071772affd2ed14a74f03b4e71b4dd1a14c3714d85aed64abcaaee5fec2efc79002ba81155c708f4df65821b444abb0cfade + languageName: node + linkType: hard + +"unique-filename@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-filename@npm:5.0.0" + dependencies: + unique-slug: "npm:^6.0.0" + checksum: 10/a5f67085caef74bdd2a6869a200ed5d68d171f5cc38435a836b5fd12cce4e4eb55e6a190298035c325053a5687ed7a3c96f0a91e82215fd14729769d9ac57d9b + languageName: node + linkType: hard + +"unique-slug@npm:^6.0.0": + version: 6.0.0 + resolution: "unique-slug@npm:6.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10/b78ed9d5b01ff465f80975f17387750ed3639909ac487fa82c4ae4326759f6de87c2131c0c39eca4c68cf06c537a8d104fba1dfc8a30308f99bc505345e1eba3 + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + +"which@npm:^6.0.0": + version: 6.0.1 + resolution: "which@npm:6.0.1" + dependencies: + isexe: "npm:^4.0.0" + bin: + node-which: bin/which.js + checksum: 10/dbea77c7d3058bf6c78bf9659d2dce4d2b57d39a15b826b2af6ac2e5a219b99dc8a831b79fdbc453c0598adb4f3f84cf9c2491fd52beb9f5d2dececcad117f68 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10/159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10/4cb02b42b8a93b5cf50caf5d8e9beb409400a8a4d85e83bb0685c1457e9ac0b7a00819e9f5991ac25ffabb56a78e2f017c1acc010b3a1babfe6de690ba531abd + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10/1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a + languageName: node + linkType: hard diff --git a/src/data/getNearbyDefibs.js b/src/data/getNearbyDefibs.js new file mode 100644 index 0000000..02332af --- /dev/null +++ b/src/data/getNearbyDefibs.js @@ -0,0 +1,65 @@ +// Final exported function to retrieve nearby defibrillators from the embedded DB. +// Usage: +// import getNearbyDefibs from "~/data/getNearbyDefibs"; +// const results = await getNearbyDefibs({ lat: 48.8566, lon: 2.3522, radiusMeters: 1000, limit: 20 }); + +import { + getNearbyDefibs as queryNearby, + getNearbyDefibsBbox, +} from "~/db/defibsRepo"; + +/** + * @typedef {Object} DefibResult + * @property {string} id + * @property {number} latitude + * @property {number} longitude + * @property {string} nom + * @property {string} adresse + * @property {string} horaires + * @property {string} acces + * @property {number} disponible_24h + * @property {number} distanceMeters + */ + +/** + * Retrieve nearby defibrillators, sorted by distance. + * Uses H3 spatial index with automatic bbox fallback. + * + * @param {Object} params + * @param {number} params.lat - User latitude (WGS84) + * @param {number} params.lon - User longitude (WGS84) + * @param {number} params.radiusMeters - Search radius in meters + * @param {number} params.limit - Maximum number of results + * @param {boolean} [params.disponible24hOnly] - Only return 24/7 accessible defibrillators + * @param {boolean} [params.progressive] - Progressive H3 ring expansion (saves queries for small radii) + * @returns {Promise} + */ +export default async function getNearbyDefibs({ + lat, + lon, + radiusMeters, + limit, + disponible24hOnly = false, + progressive = true, +}) { + try { + return await queryNearby({ + lat, + lon, + radiusMeters, + limit, + disponible24hOnly, + progressive, + }); + } catch (err) { + // Fallback to bbox if H3 fails (e.g. missing h3-js on a platform) + console.warn("H3 query failed, falling back to bbox:", err.message); + return getNearbyDefibsBbox({ + lat, + lon, + radiusMeters, + limit, + disponible24hOnly, + }); + } +} diff --git a/src/db/defibsRepo.js b/src/db/defibsRepo.js new file mode 100644 index 0000000..a37da9a --- /dev/null +++ b/src/db/defibsRepo.js @@ -0,0 +1,177 @@ +// Defibrillator repository — nearby queries with H3 geo-indexing. +import { latLngToCell, gridDisk } from "h3-js"; + +import getDb from "./openDb"; +import haversine from "~/utils/geo/haversine"; + +// H3 average edge lengths in meters per resolution (0..15). +const H3_EDGE_M = [ + 1107712, 418676, 158244, 59810, 22606, 8544, 3229, 1220, 461, 174, 65, 24, + 9, 3, 1, 0.5, +]; + +const H3_RES = 8; + +// SQLite max variable number is 999 by default; chunk IN() queries accordingly. +const SQL_VAR_LIMIT = 900; + +// Compute k (ring size) needed to cover a given radius at a given H3 resolution. +function kForRadius(radiusMeters, res = H3_RES) { + const edge = H3_EDGE_M[res]; + // sqrt(3) * edge ≈ diameter between parallel edges of a hexagon + return Math.max(1, Math.ceil(radiusMeters / (edge * Math.sqrt(3)))); +} + +// Build a bounding-box fallback SQL clause + params. +function bboxClause(lat, lon, radiusMeters) { + // 1 degree latitude ≈ 111_320 m + const dLat = radiusMeters / 111_320; + // 1 degree longitude shrinks with cos(lat) + const dLon = radiusMeters / (111_320 * Math.cos((lat * Math.PI) / 180)); + return { + clause: + "latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?", + params: [lat - dLat, lat + dLat, lon - dLon, lon + dLon], + }; +} + +/** + * @typedef {Object} Defib + * @property {string} id + * @property {number} latitude + * @property {number} longitude + * @property {string} nom + * @property {string} adresse + * @property {string} horaires + * @property {string} acces + * @property {number} disponible_24h + */ + +/** + * Fetch defibrillators near a given point. + * + * @param {Object} params + * @param {number} params.lat - User latitude + * @param {number} params.lon - User longitude + * @param {number} params.radiusMeters - Search radius in meters + * @param {number} params.limit - Max results returned + * @param {boolean} [params.disponible24hOnly] - Filter 24/7 accessible only + * @param {boolean} [params.progressive] - Enable progressive expansion (k=1,2,3…) + * @returns {Promise<(Defib & { distanceMeters: number })[]>} + */ +export async function getNearbyDefibs({ + lat, + lon, + radiusMeters, + limit, + disponible24hOnly = false, + progressive = false, +}) { + const db = await getDb(); + const maxK = kForRadius(radiusMeters); + + if (progressive) { + return progressiveSearch(db, lat, lon, radiusMeters, limit, disponible24hOnly, maxK); + } + + // One-shot: compute full disk and query + const cells = gridDisk(latLngToCell(lat, lon, H3_RES), maxK); + const candidates = await queryCells(db, cells, disponible24hOnly); + return rankAndFilter(candidates, lat, lon, radiusMeters, limit); +} + +// Progressive expansion: start at k=1, expand until enough results or maxK. +async function progressiveSearch(db, lat, lon, radiusMeters, limit, dispo24h, maxK) { + let allCandidates = []; + const seenIds = new Set(); + + for (let k = 1; k <= maxK; k++) { + const cells = gridDisk(latLngToCell(lat, lon, H3_RES), k); + const rows = await queryCells(db, cells, dispo24h); + + for (const row of rows) { + if (!seenIds.has(row.id)) { + seenIds.add(row.id); + allCandidates.push(row); + } + } + + // Early exit: if we already have more candidates than limit, rank and check + if (allCandidates.length >= limit) { + const ranked = rankAndFilter(allCandidates, lat, lon, radiusMeters, limit); + if (ranked.length >= limit) return ranked; + } + } + + return rankAndFilter(allCandidates, lat, lon, radiusMeters, limit); +} + +// Query the DB for rows matching a set of H3 cells, chunking if needed. +async function queryCells(db, cells, dispo24h) { + if (cells.length === 0) return []; + + const results = []; + + // Chunk cells to stay under SQLite variable limit + for (let i = 0; i < cells.length; i += SQL_VAR_LIMIT) { + const chunk = cells.slice(i, i + SQL_VAR_LIMIT); + const placeholders = chunk.map(() => "?").join(","); + + let sql = `SELECT id, latitude, longitude, nom, adresse, horaires, acces, disponible_24h + FROM defibs WHERE h3 IN (${placeholders})`; + const params = [...chunk]; + + if (dispo24h) { + sql += " AND disponible_24h = 1"; + } + + const rows = await db.getAllAsync(sql, params); + results.push(...rows); + } + + return results; +} + +// Compute distance, filter by radius, sort, and limit. +function rankAndFilter(candidates, lat, lon, radiusMeters, limit) { + const withDist = []; + for (const row of candidates) { + const distanceMeters = haversine(lat, lon, row.latitude, row.longitude); + if (distanceMeters <= radiusMeters) { + withDist.push({ ...row, distanceMeters }); + } + } + withDist.sort((a, b) => a.distanceMeters - b.distanceMeters); + return withDist.slice(0, limit); +} + +/** + * Bbox fallback — use when H3 is unavailable. + * + * @param {Object} params + * @param {number} params.lat + * @param {number} params.lon + * @param {number} params.radiusMeters + * @param {number} params.limit + * @param {boolean} [params.disponible24hOnly] + * @returns {Promise<(Defib & { distanceMeters: number })[]>} + */ +export async function getNearbyDefibsBbox({ + lat, + lon, + radiusMeters, + limit, + disponible24hOnly = false, +}) { + const db = await getDb(); + const { clause, params } = bboxClause(lat, lon, radiusMeters); + + let sql = `SELECT id, latitude, longitude, nom, adresse, horaires, acces, disponible_24h + FROM defibs WHERE ${clause}`; + if (disponible24hOnly) { + sql += " AND disponible_24h = 1"; + } + + const rows = await db.getAllAsync(sql, params); + return rankAndFilter(rows, lat, lon, radiusMeters, limit); +} diff --git a/src/db/openDb.js b/src/db/openDb.js new file mode 100644 index 0000000..8b5191b --- /dev/null +++ b/src/db/openDb.js @@ -0,0 +1,41 @@ +// Open the pre-built geodae SQLite database (Expo variant). +// Requires: expo-sqlite, expo-file-system, expo-asset +import * as SQLite from "expo-sqlite"; +import * as FileSystem from "expo-file-system"; +import { Asset } from "expo-asset"; + +const DB_NAME = "geodae.db"; + +let _dbPromise = null; + +export default function getDb() { + if (!_dbPromise) { + _dbPromise = initDb(); + } + return _dbPromise; +} + +async function initDb() { + const sqliteDir = `${FileSystem.documentDirectory}SQLite`; + const dbPath = `${sqliteDir}/${DB_NAME}`; + + // Ensure the SQLite directory exists + const dirInfo = await FileSystem.getInfoAsync(sqliteDir); + if (!dirInfo.exists) { + await FileSystem.makeDirectoryAsync(sqliteDir, { intermediates: true }); + } + + // Copy asset DB on first launch (or after app update clears documents) + const fileInfo = await FileSystem.getInfoAsync(dbPath); + if (!fileInfo.exists) { + const asset = Asset.fromModule(require("../assets/db/geodae.db")); + await asset.downloadAsync(); + await FileSystem.copyAsync({ from: asset.localUri, to: dbPath }); + } + + const db = await SQLite.openDatabaseAsync(DB_NAME); + // Read-only optimizations + await db.execAsync("PRAGMA journal_mode = WAL"); + await db.execAsync("PRAGMA cache_size = -8000"); // 8 MB + return db; +} diff --git a/src/db/openDb.op-sqlite.js b/src/db/openDb.op-sqlite.js new file mode 100644 index 0000000..52a60d3 --- /dev/null +++ b/src/db/openDb.op-sqlite.js @@ -0,0 +1,17 @@ +// Open the pre-built geodae SQLite database (Bare RN variant). +// Requires: @op-engineering/op-sqlite +// Install: npm install @op-engineering/op-sqlite +// Place geodae.db in: +// Android: android/app/src/main/assets/geodae.db +// iOS: add geodae.db to Xcode project "Copy Bundle Resources" +import { open } from "@op-engineering/op-sqlite"; + +let _db = null; + +export default function getDb() { + if (!_db) { + _db = open({ name: "geodae.db", readOnly: true }); + _db.execute("PRAGMA cache_size = -8000"); // 8 MB + } + return _db; +} diff --git a/src/utils/geo/haversine.js b/src/utils/geo/haversine.js new file mode 100644 index 0000000..1a7addc --- /dev/null +++ b/src/utils/geo/haversine.js @@ -0,0 +1,14 @@ +// Haversine distance in meters between two WGS84 points. +const DEG_TO_RAD = Math.PI / 180; +const EARTH_RADIUS_M = 6_371_000; + +export default function haversine(lat1, lon1, lat2, lon2) { + const dLat = (lat2 - lat1) * DEG_TO_RAD; + const dLon = (lon2 - lon1) * DEG_TO_RAD; + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(lat1 * DEG_TO_RAD) * + Math.cos(lat2 * DEG_TO_RAD) * + Math.sin(dLon / 2) ** 2; + return 2 * EARTH_RADIUS_M * Math.asin(Math.sqrt(a)); +}