ops: track tif_polygons.ts orphan import
entity.ts imports findTifDistrict from ./tif_polygons.js but the source file was never committed — only present in the working tree. Adding it so a fresh clone compiles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
528fded11b
commit
51cc0a69cf
178
mcp-server/tif_polygons.ts
Normal file
178
mcp-server/tif_polygons.ts
Normal file
@ -0,0 +1,178 @@
|
||||
// TIF (Tax Increment Financing) district point-in-polygon lookup.
|
||||
// Given a property's lat/long, returns which Chicago TIF district (if
|
||||
// any) contains it. TIF districts are public-subsidy zones — a property
|
||||
// inside one is receiving city tax-increment funding for its build.
|
||||
// Strong "this project has financial backing" signal for the Project Index.
|
||||
//
|
||||
// Data: data/_entity_cache/tif_districts.geojson (Chicago Open Data
|
||||
// dataset eejr-xtfb, 100 active districts, 3.2MB). Refresh by re-running
|
||||
// `curl ... eejr-xtfb.geojson > tif_districts.geojson` — districts
|
||||
// change rarely (only when city council approves new ones or repeals).
|
||||
//
|
||||
// Algorithm: classic ray-casting. For each MultiPolygon's outer ring,
|
||||
// count edge crossings of an east-going horizontal ray from the point.
|
||||
// Odd crossings = inside. Holes (inner rings) flip the parity. Library-
|
||||
// free; correct for arbitrary polygons including the irregular Chicago
|
||||
// shapes which often have many small detours.
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const TIF_GEOJSON = join("/home/profit/lakehouse/data/_entity_cache", "tif_districts.geojson");
|
||||
|
||||
type LngLat = [number, number]; // GeoJSON convention: [longitude, latitude]
|
||||
type Ring = LngLat[];
|
||||
type Polygon = Ring[]; // outer ring + optional inner rings (holes)
|
||||
type MultiPolygon = Polygon[];
|
||||
|
||||
type TifFeature = {
|
||||
name: string;
|
||||
trim_name?: string;
|
||||
ref?: string;
|
||||
approval_date?: string;
|
||||
expiration?: string;
|
||||
type?: string; // T-1xx etc.
|
||||
comm_area?: string;
|
||||
wards?: string;
|
||||
// Bounding box for quick reject
|
||||
bbox: { minLon: number; minLat: number; maxLon: number; maxLat: number };
|
||||
geometry: MultiPolygon;
|
||||
};
|
||||
|
||||
let tifIdx: TifFeature[] | null = null;
|
||||
|
||||
function bboxOfMultiPolygon(mp: MultiPolygon): TifFeature["bbox"] {
|
||||
let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
|
||||
for (const poly of mp) {
|
||||
for (const ring of poly) {
|
||||
for (const [lon, lat] of ring) {
|
||||
if (lon < minLon) minLon = lon;
|
||||
if (lat < minLat) minLat = lat;
|
||||
if (lon > maxLon) maxLon = lon;
|
||||
if (lat > maxLat) maxLat = lat;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { minLon, minLat, maxLon, maxLat };
|
||||
}
|
||||
|
||||
async function ensureLoaded(): Promise<TifFeature[]> {
|
||||
if (tifIdx) return tifIdx;
|
||||
if (!existsSync(TIF_GEOJSON)) {
|
||||
tifIdx = [];
|
||||
return tifIdx;
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(await readFile(TIF_GEOJSON, "utf-8"));
|
||||
const out: TifFeature[] = [];
|
||||
for (const f of raw.features || []) {
|
||||
const geom = f.geometry;
|
||||
if (!geom) continue;
|
||||
// Normalize Polygon → MultiPolygon for uniform iteration
|
||||
let mp: MultiPolygon;
|
||||
if (geom.type === "MultiPolygon") {
|
||||
mp = geom.coordinates;
|
||||
} else if (geom.type === "Polygon") {
|
||||
mp = [geom.coordinates];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
const props = f.properties || {};
|
||||
out.push({
|
||||
name: props.name || "Unknown TIF",
|
||||
trim_name: props.name_trim,
|
||||
ref: props.ref,
|
||||
approval_date: props.approval_d,
|
||||
expiration: props.expiration,
|
||||
type: props.type,
|
||||
comm_area: props.comm_area,
|
||||
wards: props.wards,
|
||||
bbox: bboxOfMultiPolygon(mp),
|
||||
geometry: mp,
|
||||
});
|
||||
}
|
||||
tifIdx = out;
|
||||
return tifIdx;
|
||||
} catch (e) {
|
||||
console.warn("[tif] load failed:", (e as Error).message);
|
||||
tifIdx = [];
|
||||
return tifIdx;
|
||||
}
|
||||
}
|
||||
|
||||
// Ray-casting point-in-polygon (single ring). Returns true if (lon, lat)
|
||||
// is strictly inside the ring. Edge cases (point exactly on a vertex)
|
||||
// resolve by half-open interval convention; for our use case (Chicago
|
||||
// boundary precision is ~1m, sites are point queries) this is fine.
|
||||
function pointInRing(lon: number, lat: number, ring: Ring): boolean {
|
||||
let inside = false;
|
||||
const n = ring.length;
|
||||
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||
const [xi, yi] = ring[i];
|
||||
const [xj, yj] = ring[j];
|
||||
const intersect =
|
||||
yi > lat !== yj > lat &&
|
||||
lon < ((xj - xi) * (lat - yi)) / (yj - yi + 0) + xi;
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// Polygon = outer ring + holes. Inside outer AND not inside any hole.
|
||||
function pointInPolygon(lon: number, lat: number, polygon: Polygon): boolean {
|
||||
if (polygon.length === 0) return false;
|
||||
if (!pointInRing(lon, lat, polygon[0])) return false;
|
||||
for (let i = 1; i < polygon.length; i++) {
|
||||
if (pointInRing(lon, lat, polygon[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export type TifMatch = {
|
||||
name: string;
|
||||
ref?: string;
|
||||
approval_date?: string;
|
||||
expiration?: string;
|
||||
comm_area?: string;
|
||||
wards?: string;
|
||||
};
|
||||
|
||||
export async function findTifDistrict(
|
||||
longitude: number | string | undefined,
|
||||
latitude: number | string | undefined,
|
||||
): Promise<TifMatch | null> {
|
||||
const lon = typeof longitude === "string" ? parseFloat(longitude) : longitude;
|
||||
const lat = typeof latitude === "string" ? parseFloat(latitude) : latitude;
|
||||
if (!lon || !lat || isNaN(lon) || isNaN(lat)) return null;
|
||||
const idx = await ensureLoaded();
|
||||
if (idx.length === 0) return null;
|
||||
for (const f of idx) {
|
||||
// Bbox reject — cheap O(1) skip for the 99% of districts that
|
||||
// can't possibly contain the point.
|
||||
const b = f.bbox;
|
||||
if (lon < b.minLon || lon > b.maxLon || lat < b.minLat || lat > b.maxLat) continue;
|
||||
// Full point-in-polygon for any polygon in this MultiPolygon
|
||||
for (const poly of f.geometry) {
|
||||
if (pointInPolygon(lon, lat, poly)) {
|
||||
return {
|
||||
name: f.name,
|
||||
ref: f.ref,
|
||||
approval_date: f.approval_date,
|
||||
expiration: f.expiration,
|
||||
comm_area: f.comm_area,
|
||||
wards: f.wards,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getTifIndexStats(): Promise<{
|
||||
total: number;
|
||||
loaded: boolean;
|
||||
}> {
|
||||
const idx = await ensureLoaded();
|
||||
return { total: idx.length, loaded: idx.length > 0 };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user