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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user