Public Access
1
0

feat: add deep research planning and postal batch runs

Add a dedicated Deep Research view with postal-area preview overlays, batch execution, and bundled map results.

Also add postal dataset import tooling and fix local API networking and research insert issues needed to support the new workflow.
This commit is contained in:
pguerrerox
2026-04-05 18:05:04 +00:00
parent a1ba5ee093
commit cc00a439bf
29 changed files with 1860 additions and 82 deletions
+32
View File
@@ -0,0 +1,32 @@
import { getDbPool } from '../../server/src/db/pool.js';
async function run() {
const pool = getDbPool();
const client = await pool.connect();
try {
await client.query('begin');
await client.query('truncate table public.postal_area_neighbors');
await client.query(`
insert into public.postal_area_neighbors (postal_area_id, neighbor_postal_area_id)
select source.id, neighbor.id
from public.postal_areas source
join public.postal_areas neighbor
on source.country_code = neighbor.country_code
and source.id <> neighbor.id
and ST_Touches(source.geom, neighbor.geom)
`);
await client.query('commit');
const summary = await client.query<{ count: string }>('select count(*)::text as count from public.postal_area_neighbors');
console.log(`Built ${summary.rows[0]?.count ?? '0'} postal adjacency links.`);
} catch (error) {
await client.query('rollback');
throw error;
} finally {
client.release();
await pool.end();
}
}
await run();
+121
View File
@@ -0,0 +1,121 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getDbPool } from '../../server/src/db/pool.js';
import { getFeatureGeometry, getStringProperty, normalizePostalCode, readFeatureCollection, type PostalDatasetConfig } from './postal-import-utils.js';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const datasetsRoot = path.resolve(currentDir, '../datasets/postal');
const datasetConfigs: PostalDatasetConfig[] = [
{
countryCode: 'US',
label: 'US ZIP/ZCTA',
filePath: process.env.POSTAL_US_DATASET_PATH || path.join(datasetsRoot, 'us_zcta.geojson'),
postalCodeKeys: ['postal_code', 'zip', 'zcta', 'GEOID20', 'ZCTA5CE20', 'ZCTA5CE10'],
displayNameKeys: ['display_name', 'name', 'NAMELSAD20', 'GEOID20', 'postal_code'],
},
{
countryCode: 'CA',
label: 'Canada FSA',
filePath: process.env.POSTAL_CA_DATASET_PATH || path.join(datasetsRoot, 'ca_fsa.geojson'),
postalCodeKeys: ['postal_code', 'fsa', 'CFSAUID', 'CFSAUID24'],
displayNameKeys: ['display_name', 'name', 'CFSAUID', 'postal_code'],
},
];
async function importDataset(config: PostalDatasetConfig) {
const pool = getDbPool();
const client = await pool.connect();
try {
const collection = await readFeatureCollection(config.filePath);
let insertedCount = 0;
let skippedCount = 0;
await client.query('begin');
for (const [index, feature] of collection.features.entries()) {
const rawPostalCode = getStringProperty(feature.properties, config.postalCodeKeys);
if (!rawPostalCode) {
skippedCount += 1;
continue;
}
const normalizedPostalCode = normalizePostalCode(config.countryCode, rawPostalCode);
if (!normalizedPostalCode) {
skippedCount += 1;
continue;
}
const displayName = getStringProperty(feature.properties, config.displayNameKeys) || normalizedPostalCode;
const geometry = getFeatureGeometry(feature, config.filePath, index);
await client.query(
`
insert into public.postal_areas (
country_code,
postal_code,
display_name,
normalized_postal_code,
geom,
centroid,
search_radius_m,
metadata_json,
created_at,
updated_at
)
values (
$1,
$2,
$3,
$4,
ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON($5), 4326)),
ST_Centroid(ST_SetSRID(ST_GeomFromGeoJSON($5), 4326))::geography,
greatest(1000, round(sqrt(ST_Area(ST_SetSRID(ST_GeomFromGeoJSON($5), 4326)::geography) / pi()))::integer),
$6::jsonb,
now(),
now()
)
on conflict (country_code, normalized_postal_code)
do update set
postal_code = excluded.postal_code,
display_name = excluded.display_name,
geom = excluded.geom,
centroid = excluded.centroid,
search_radius_m = excluded.search_radius_m,
metadata_json = excluded.metadata_json,
updated_at = now()
`,
[
config.countryCode,
rawPostalCode.trim(),
displayName,
normalizedPostalCode,
JSON.stringify(geometry),
JSON.stringify(feature.properties ?? {}),
],
);
insertedCount += 1;
}
await client.query('commit');
console.log(`Imported ${insertedCount} ${config.label} areas from ${config.filePath}. Skipped ${skippedCount}.`);
} catch (error) {
await client.query('rollback');
throw error;
} finally {
client.release();
}
}
async function run() {
for (const config of datasetConfigs) {
await importDataset(config);
}
await getDbPool().end();
}
await run();
-1
View File
@@ -1,4 +1,3 @@
import 'dotenv/config';
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
+76
View File
@@ -0,0 +1,76 @@
import { readFile } from 'node:fs/promises';
export type FeatureGeometry = {
type: 'Polygon' | 'MultiPolygon';
coordinates: unknown;
};
export type GeoJsonFeature = {
type: 'Feature';
properties?: Record<string, unknown> | null;
geometry?: FeatureGeometry | null;
};
export type GeoJsonFeatureCollection = {
type: 'FeatureCollection';
features: GeoJsonFeature[];
};
export type PostalDatasetConfig = {
countryCode: 'US' | 'CA';
label: string;
filePath: string;
postalCodeKeys: string[];
displayNameKeys: string[];
};
export async function readFeatureCollection(filePath: string) {
const raw = await readFile(filePath, 'utf8');
const parsed = JSON.parse(raw) as GeoJsonFeatureCollection;
if (parsed.type !== 'FeatureCollection' || !Array.isArray(parsed.features)) {
throw new Error(`Dataset at ${filePath} is not a valid GeoJSON FeatureCollection.`);
}
return parsed;
}
export function normalizePostalCode(countryCode: 'US' | 'CA', rawPostalCode: string) {
const trimmed = rawPostalCode.trim();
if (countryCode === 'US') {
const digits = trimmed.replace(/\D/g, '').slice(0, 5);
return digits.length === 5 ? digits : null;
}
const compact = trimmed.replace(/\s+/g, '').toUpperCase();
return compact.length >= 3 ? compact.slice(0, 3) : null;
}
export function getStringProperty(properties: Record<string, unknown> | null | undefined, keys: string[]) {
if (!properties) {
return null;
}
for (const key of keys) {
const value = properties[key];
if (typeof value === 'string' && value.trim().length > 0) {
return value.trim();
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value);
}
}
return null;
}
export function getFeatureGeometry(feature: GeoJsonFeature, filePath: string, index: number) {
if (!feature.geometry || (feature.geometry.type !== 'Polygon' && feature.geometry.type !== 'MultiPolygon')) {
throw new Error(`Feature ${index} in ${filePath} is missing a Polygon or MultiPolygon geometry.`);
}
return feature.geometry;
}
-2
View File
@@ -1,2 +0,0 @@
console.log('Postal dataset import is not implemented yet.');
console.log('Next step: add ZIP/ZCTA and Canada FSA import scripts into db/datasets and db/scripts.');