← back to Nationalrealestate
src/server/commercial.ts
179 lines
/**
* LA-region commercial real-estate API — the "local commercial RE news" layer.
* Reads the materialized `commercial_parcel` table (see 005_commercial.sql +
* src/ingest/commercial/la_assessor.ts). LA carries assessed values + last
* recording date but NO sale price / owner (assessor-roll limits), so the feed is
* "recorded activity + assessed value" — never fabricated prices.
*
* GET /api/commercial/types -> taxonomy + per-type counts
* GET /api/commercial/search?type&city&q&min&max&sort&limit&offset
* GET /api/commercial/feed?type&city&limit -> recent recorded activity
* GET /api/commercial/marquee?type&city&limit -> top assets by assessed value
* GET /api/commercial/property/:ain -> single-parcel detail + context
*/
import type { Express, Request, Response } from 'express';
import { query } from '../../db/pool.ts';
import { COMMERCIAL_CATEGORIES, categoryLabel, type CommercialType } from '../lib/commercial_types.ts';
const COUNTY = '06037'; // LA County (first region; multi-county-ready via ?county)
const likeEscape = (s: string) => s.replace(/[\\%_]/g, c => '\\' + c);
const TYPE_KEYS = new Set(COMMERCIAL_CATEGORIES.map(c => c.key));
const clampLimit = (v: unknown, def: number, max: number) => Math.min(max, Math.max(1, Number(v) || def));
/** shared WHERE builder for type/city/q/value filters */
function buildFilter(q: Request['query']): { where: string; params: unknown[] } {
const county = String(q.county || COUNTY);
const params: unknown[] = [county];
const parts = ['county_fips = $1'];
if (q.type && TYPE_KEYS.has(String(q.type) as CommercialType)) { params.push(String(q.type)); parts.push(`ctype = $${params.length}`); }
if (q.city) { params.push(String(q.city).toUpperCase()); parts.push(`city = $${params.length}`); }
if (q.q) { params.push('%' + likeEscape(String(q.q).toUpperCase()) + '%'); parts.push(`(upper(address) LIKE $${params.length} ESCAPE '\\' OR upper(city) LIKE $${params.length} ESCAPE '\\')`); }
if (q.min) { params.push(Number(q.min)); parts.push(`assessed_total >= $${params.length}`); }
if (q.max) { params.push(Number(q.max)); parts.push(`assessed_total <= $${params.length}`); }
return { where: parts.join(' AND '), params };
}
const SORTS: Record<string, string> = {
value_desc: 'assessed_total DESC NULLS LAST',
value_asc: 'assessed_total ASC NULLS LAST',
recent: 'recording_date DESC NULLS LAST',
sqft_desc: 'sqft DESC NULLS LAST',
};
function shape(r: any) {
return {
ain: r.ain, address: r.address, city: r.city, zip: r.zip,
type: r.ctype, type_label: categoryLabel(r.ctype), use_desc: r.use_desc,
assessed_total: r.assessed_total != null ? Number(r.assessed_total) : null,
assessed_land: r.assessed_land != null ? Number(r.assessed_land) : null,
assessed_improvement: r.assessed_imp != null ? Number(r.assessed_imp) : null,
roll_year: r.roll_year, recording_date: r.recording_date,
sqft: r.sqft, year_built: r.year_built, units: r.units,
};
}
export function mountCommercial(app: Express) {
// taxonomy + per-type counts (drives the filter UI)
app.get('/api/commercial/types', async (req: Request, res: Response) => {
try {
const county = String(req.query.county || COUNTY);
const r = await query<{ ctype: string; n: string }>(
`SELECT ctype, count(*)::text n FROM commercial_parcel WHERE county_fips=$1 GROUP BY ctype`, [county]);
const counts = Object.fromEntries(r.rows.map(x => [x.ctype, Number(x.n)]));
const total = r.rows.reduce((s, x) => s + Number(x.n), 0);
res.json({ county, total, types: COMMERCIAL_CATEGORIES.map(c => ({ ...c, count: counts[c.key] || 0 })) });
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
// top cities by commercial count (for the filter dropdown)
app.get('/api/commercial/cities', async (req: Request, res: Response) => {
try {
const county = String(req.query.county || COUNTY);
const r = await query<{ city: string; n: string }>(
`SELECT city, count(*)::text n FROM commercial_parcel WHERE county_fips=$1 AND city IS NOT NULL
GROUP BY city ORDER BY count(*) DESC LIMIT 60`, [county]);
res.json({ cities: r.rows.map(x => ({ city: x.city, count: Number(x.n) })) });
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
// filtered browse/search
app.get('/api/commercial/search', async (req: Request, res: Response) => {
try {
const { where, params } = buildFilter(req.query);
const sort = SORTS[String(req.query.sort || 'value_desc')] || SORTS.value_desc;
const limit = clampLimit(req.query.limit, 60, 200);
const offset = Math.max(0, Number(req.query.offset) || 0);
const total = await query<{ n: string }>(`SELECT count(*)::text n FROM commercial_parcel WHERE ${where}`, params);
const rows = await query(
`SELECT * FROM commercial_parcel WHERE ${where} ORDER BY ${sort}, ain LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
[...params, limit, offset]);
res.json({ total: Number(total.rows[0].n), count: rows.rows.length, offset, results: rows.rows.map(shape) });
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
// recent recorded activity — the "news feed" (recording date, no price for LA)
app.get('/api/commercial/feed', async (req: Request, res: Response) => {
try {
const { where, params } = buildFilter(req.query);
const limit = clampLimit(req.query.limit, 40, 100);
const rows = await query(
`SELECT p.*, b.brief FROM commercial_parcel p
LEFT JOIN commercial_brief b ON b.county_fips=p.county_fips AND b.ain=p.ain
WHERE ${where.replace(/\b(county_fips|ctype|city|address|assessed_total)\b/g, 'p.$1')}
AND p.recording_date IS NOT NULL
ORDER BY p.recording_date DESC, p.assessed_total DESC NULLS LAST LIMIT $${params.length + 1}`,
[...params, limit]);
res.json({ count: rows.rows.length, results: rows.rows.map(r => ({ ...shape(r), brief: r.brief || null })) });
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
// marquee assets — top by assessed value (the newsworthy trophy hook), with briefs
app.get('/api/commercial/marquee', async (req: Request, res: Response) => {
try {
const { where, params } = buildFilter(req.query);
const limit = clampLimit(req.query.limit, 12, 50);
const rows = await query(
`SELECT p.*, b.brief FROM commercial_parcel p
LEFT JOIN commercial_brief b ON b.county_fips=p.county_fips AND b.ain=p.ain
WHERE ${where.replace(/\b(county_fips|ctype|city|address|assessed_total)\b/g, 'p.$1')}
AND p.assessed_total IS NOT NULL
ORDER BY p.assessed_total DESC LIMIT $${params.length + 1}`, [...params, limit]);
res.json({ count: rows.rows.length, results: rows.rows.map(r => ({ ...shape(r), brief: r.brief || null })) });
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
// one-shot digest for syndication (e.g. RENTV): marquee+briefs, recent activity, top LA firms
app.get('/api/commercial/digest', async (req: Request, res: Response) => {
try {
const county = String(req.query.county || COUNTY);
const [marquee, recent, firms] = await Promise.all([
query(`SELECT p.*, b.brief FROM commercial_parcel p
LEFT JOIN commercial_brief b ON b.county_fips=p.county_fips AND b.ain=p.ain
WHERE p.county_fips=$1 AND p.assessed_total IS NOT NULL AND b.brief IS NOT NULL
ORDER BY p.assessed_total DESC LIMIT 12`, [county]),
query(`SELECT p.*, b.brief FROM commercial_parcel p
LEFT JOIN commercial_brief b ON b.county_fips=p.county_fips AND b.ain=p.ain
WHERE p.county_fips=$1 AND p.recording_date IS NOT NULL
ORDER BY p.recording_date DESC, p.assessed_total DESC NULLS LAST LIMIT 20`, [county]),
query(`SELECT name, agent_count, hq_city, license_no FROM firm
WHERE license_state='CA' AND upper(hq_city)='LOS ANGELES' AND agent_count>0
ORDER BY agent_count DESC NULLS LAST LIMIT 12`),
]);
res.json({
fetched_at: new Date().toISOString(), county,
marquee: marquee.rows.map(r => ({ ...shape(r), brief: r.brief || null })),
recent: recent.rows.map(r => ({ ...shape(r), brief: r.brief || null })),
firms: firms.rows.map((f: any) => ({ name: f.name, agent_count: f.agent_count, city: f.hq_city, license_no: f.license_no })),
});
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
// generated news blurb for one asset (precomputed by ingest:commercial-briefs)
app.get('/api/commercial/brief/:ain', async (req: Request, res: Response) => {
try {
const county = String(req.query.county || COUNTY);
const r = await query<{ brief: string; model: string; generated_at: string }>(
`SELECT brief, model, generated_at FROM commercial_brief WHERE county_fips=$1 AND ain=$2`, [county, req.params.ain]);
if (!r.rows.length) return res.status(404).json({ error: 'no brief for this asset' });
res.json(r.rows[0]);
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
// single-parcel detail + county market context
app.get('/api/commercial/property/:ain', async (req: Request, res: Response) => {
try {
const county = String(req.query.county || COUNTY);
const r = await query(
`SELECT p.*, b.brief FROM commercial_parcel p
LEFT JOIN commercial_brief b ON b.county_fips=p.county_fips AND b.ain=p.ain
WHERE p.county_fips=$1 AND p.ain=$2`, [county, req.params.ain]);
if (!r.rows.length) return res.status(404).json({ error: 'not found' });
const p = { ...shape(r.rows[0]), brief: r.rows[0].brief || null };
res.json({
...p,
pricing_note: 'LA County assessor roll — assessed values + last recording date only. Sale price and owner are not in the public roll; market listing prices come from the commercial listings feed.',
});
} catch (e: any) { res.status(500).json({ error: String(e.message || e) }); }
});
}