← back to Petitionyour
lib/reps.js
237 lines
// $0 / free-only representative lookup + resolution.
//
// Two-layer design, honesty over false precision:
//
// 1. ZIP -> STATE: deterministic, offline, bundled USPS ZIP3-prefix range
// table. Good enough to say "you're in California" for every 5-digit ZIP.
//
// 2. STATE + (district) -> ACTUAL MEMBERS: resolved against the bundled
// public-domain `unitedstates/congress-legislators` dataset (see
// scripts/refresh-legislators.js). SENATORS are resolved EXACTLY from the
// state (every state has exactly 2). The HOUSE rep needs a congressional
// district, which a ZIP alone cannot give unambiguously:
// - Single-district ("at-large") states -> exactly 1 rep, unambiguous.
// - Multi-district states -> we DO NOT guess. The caller can supply a
// precise district (from the free U.S. Census geocoder, via a street
// address) to pin the exact rep; otherwise we hand back the whole
// state House delegation as *candidates* and say so plainly.
//
// We refuse to hardcode a stale name list (elections change officeholders);
// instead we ship a refreshable dataset + a refresh script. This is the only
// path that is both specific AND can't silently rot into misinformation.
const path = require('path');
const ranges = require('../data/zip3-state-ranges.json');
// ---- bundled member dataset (loaded once, cached) ----
let _members = null;
function loadMembers() {
if (_members) return _members;
// Resolve lazily so a missing/renamed file surfaces at call time, not require time.
const raw = require(path.join(__dirname, '..', 'data', 'legislators-current.json'));
_members = raw.map(normalizeMember).filter(Boolean);
return _members;
}
function currentTerm(m) {
return m.terms && m.terms.length ? m.terms[m.terms.length - 1] : null;
}
function normalizeMember(m) {
const t = currentTerm(m);
if (!t) return null;
const name = (m.name && (m.name.official_full ||
[m.name.first, m.name.last].filter(Boolean).join(' '))) || 'Unknown';
return {
bioguide: (m.id && m.id.bioguide) || null,
name,
party: t.party || '',
chamber: t.type === 'sen' ? 'senate' : 'house',
state: t.state,
// district is only meaningful for the House; 0 == at-large / territory delegate
district: t.type === 'rep' ? (typeof t.district === 'number' ? t.district : null) : null,
stateRank: t.state_rank || null, // 'senior' | 'junior' for senators
url: t.url || null,
contactForm: t.contact_form || null,
phone: t.phone || null,
office: t.office || null,
address: t.address || null,
};
}
// The chamber-appropriate action link we send people to. Prefer the official
// contact form; fall back to the member's official site.
function contactUrl(mem) {
return mem.contactForm || mem.url || null;
}
// States/territories with a single, at-large House seat -> the House rep is
// unambiguous from the state alone (no district needed).
const AT_LARGE = new Set(['AK', 'DE', 'ND', 'SD', 'VT', 'WY', 'DC', 'PR', 'GU', 'VI', 'AS', 'MP']);
// ---- ZIP -> state ----
function resolveZip(zip) {
const clean = String(zip || '').trim();
const m = clean.match(/^(\d{5})/);
if (!m) return null;
const prefix = parseInt(m[1].slice(0, 3), 10);
const hit = ranges.find((r) => prefix >= r.start && prefix <= r.end);
if (!hit) return null;
return { zip: m[1], stateAbbr: hit.state, stateName: hit.name };
}
// ---- member lookups ----
function senatorsForState(stateAbbr) {
return loadMembers()
.filter((m) => m.chamber === 'senate' && m.state === stateAbbr)
// senior first, then by name — stable, readable order
.sort((a, b) => (a.stateRank === 'senior' ? -1 : 1) - (b.stateRank === 'senior' ? -1 : 1));
}
function houseForState(stateAbbr) {
return loadMembers()
.filter((m) => m.chamber === 'house' && m.state === stateAbbr)
.sort((a, b) => (a.district || 0) - (b.district || 0));
}
function houseForDistrict(stateAbbr, district) {
const d = Number(district);
return loadMembers().find(
(m) => m.chamber === 'house' && m.state === stateAbbr && m.district === d
) || null;
}
// Shape a member for JSON / templates.
function publicMember(mem, extra) {
if (!mem) return null;
return {
bioguide: mem.bioguide,
name: mem.name,
party: mem.party,
chamber: mem.chamber,
state: mem.state,
district: mem.district,
stateRank: mem.stateRank,
url: mem.url,
contactForm: mem.contactForm,
contactUrl: contactUrl(mem),
phone: mem.phone,
office: mem.office,
address: mem.address,
...(extra || {}),
};
}
// Core resolver. Given a zip (required) and optional explicit district, return
// senators (exact) + a house resolution with an honest `houseMode`:
// 'exact' district was supplied (or at-large) -> one confirmed rep
// 'candidates' multi-district state, no district -> full delegation offered
function resolveReps({ zip, district } = {}) {
const hit = resolveZip(zip);
if (!hit) return null;
const senators = senatorsForState(hit.stateAbbr).map((m) => publicMember(m));
const delegation = houseForState(hit.stateAbbr);
let houseMode = 'candidates';
let houseReps = [];
if (district !== undefined && district !== null && district !== '') {
const rep = houseForDistrict(hit.stateAbbr, district);
if (rep) {
houseMode = 'exact';
houseReps = [publicMember(rep, { resolvedBy: 'address' })];
}
}
if (!houseReps.length) {
if (AT_LARGE.has(hit.stateAbbr) && delegation.length === 1) {
houseMode = 'exact';
houseReps = [publicMember(delegation[0], { resolvedBy: 'at-large' })];
} else {
houseMode = 'candidates';
houseReps = delegation.map((m) => publicMember(m));
}
}
return {
zip: hit.zip,
stateAbbr: hit.stateAbbr,
stateName: hit.stateName,
atLarge: AT_LARGE.has(hit.stateAbbr),
senators,
house: { mode: houseMode, count: houseReps.length, reps: houseReps },
};
}
// ---- free Census geocoder: full street address -> congressional district ----
// geocoding.geo.census.gov, no API key, returns the current (119th) CD.
async function geocodeAddressToDistrict(address) {
const clean = String(address || '').trim();
if (!clean) return { ok: false, error: 'No address provided.' };
const url =
'https://geocoding.geo.census.gov/geocoder/geographies/onelineaddress' +
'?address=' + encodeURIComponent(clean) +
'&benchmark=Public_AR_Current&vintage=Current_Current' +
'&layers=all&format=json';
try {
const res = await fetch(url, { headers: { 'User-Agent': 'petitionyour' } });
if (!res.ok) return { ok: false, error: `Census geocoder HTTP ${res.status}` };
const data = await res.json();
const matches = data && data.result && data.result.addressMatches;
if (!matches || !matches.length) {
return { ok: false, error: 'Address not found. Check the street, city, and state.' };
}
const geos = matches[0].geographies || {};
// The CD layer key is vintage-named, e.g. "119th Congressional Districts".
const cdKey = Object.keys(geos).find((k) => /Congressional Districts?/i.test(k));
if (!cdKey || !geos[cdKey] || !geos[cdKey].length) {
return { ok: false, error: 'Could not determine a congressional district for that address.' };
}
const cd = geos[cdKey][0];
// District number lives in a vintage field like CD119 / CD118; BASENAME is
// the human label ("4", or "at Large"). Pull the numeric where possible.
const cdNumRaw = Object.keys(cd).filter((k) => /^CD\d+$/.test(k)).map((k) => cd[k])[0];
let district = null;
if (cdNumRaw !== undefined) {
// "98"/"99"/"00" are non-voting/at-large sentinels in Census data.
const n = parseInt(cdNumRaw, 10);
district = Number.isFinite(n) ? (n >= 98 ? 0 : n) : null;
}
const stateFips = cd.STATE || (matches[0].geographies['States'] && matches[0].geographies['States'][0] && matches[0].geographies['States'][0].STATE) || null;
return {
ok: true,
district,
districtLabel: cd.BASENAME || (district != null ? String(district) : null),
stateFips,
matchedAddress: matches[0].matchedAddress || clean,
};
} catch (err) {
return { ok: false, error: 'Census geocoder request failed: ' + err.message };
}
}
// Kept for backward compatibility with the existing find-reps view/API: the
// static official .gov links (always-current fallbacks / cross-checks).
function officialLinks(stateAbbr, zip) {
return {
findYourHouseRep: 'https://www.house.gov/representatives/find-your-representative',
senateContactList: 'https://www.senate.gov/senators/senators-contact.htm',
usaGovElectedOfficials: 'https://www.usa.gov/elected-officials',
stateGovernmentSite: stateAbbr ? 'https://www.usa.gov/state-governments' : null,
congressMemberSearch: 'https://www.congress.gov/members',
};
}
module.exports = {
resolveZip,
officialLinks,
loadMembers,
senatorsForState,
houseForState,
houseForDistrict,
resolveReps,
geocodeAddressToDistrict,
AT_LARGE,
};