// PGA Lookup widget // 1. Geocode the address via Census Bureau (CORS-friendly) // 2. Pull ASCE 7-16 design parameters from USGS // Both endpoints support CORS in browsers. const PGA_STORAGE_KEY = 'seisquake.pga.lastLookup'; // When the site is hosted on a server with PHP (e.g. Hostinger), point the // widget at the bundled PHP proxy — it does the Census + USGS calls // server-side, dodging all CORS issues and coalescing two requests into one. // Set to null to force direct browser-side calls instead (works on // localhost + most modern hosts that allow CORS to USGS / Census). const HAZARD_PROXY_URL = 'api/hazard.php'; function PgaWidget() { // Start with an empty widget on every page load — previous behavior of // pre-filling from localStorage was confusing for repeat visitors. // We still write to localStorage on lookup so future history features // (e.g. recent-lookups list) can read it. React.useEffect(() => { try { localStorage.removeItem(PGA_STORAGE_KEY); } catch (e) {} }, []); const [address, setAddress] = React.useState(''); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); const [result, setResult] = React.useState(null); const [copied, setCopied] = React.useState(false); const lookup = async (e) => { e?.preventDefault?.(); if (!address.trim()) return; setLoading(true); setError(''); setResult(null); setCopied(false); let matched, lat, lon, r; try { // ── Path A: server-side proxy (one request, no CORS). // ── Path B: direct browser calls (CORS depends on origin). if (HAZARD_PROXY_URL) { const proxyUrl = `${HAZARD_PROXY_URL}?address=${encodeURIComponent(address.trim())}`; const resp = await fetch(proxyUrl); const ct = resp.headers.get('content-type') || ''; if (!ct.includes('application/json')) { // PHP didn't execute (probably 404 falling back to index.html, or // PHP isn't installed). Tell the user *something* useful. const head = (await resp.text().catch(() => '')).slice(0, 80); throw new Error( `Proxy returned non-JSON response (HTTP ${resp.status}). ` + `Make sure api/hazard.php exists on the server and PHP is enabled. ` + (head ? `First bytes: ${head}…` : '') ); } const json = await resp.json().catch(() => ({})); if (!resp.ok) { if (resp.status === 404 && json.error) { setError(json.error); setLoading(false); return; } throw new Error(json.error || `Proxy returned HTTP ${resp.status}`); } matched = json.address; lat = json.lat; lon = json.lon; r = json.data; // The proxy also returns an exact 10%/50yr PGA computed from the // USGS NSHM hazard curve (interpolated at AFE = 1/475 yr). Pass it // through so the widget can display the real Fannie-Mae-trigger // value, not an approximation. if (json.pga_10in50 != null) { r = { ...r, pga_10in50_real: json.pga_10in50 }; } console.info('[SeisQuake/PGA] proxy response:', json); if (!r || typeof r !== 'object') { throw new Error( 'Proxy returned JSON but no hazard data. ' + 'Hit the proxy URL directly in your browser to inspect: ' + proxyUrl ); } } else { // ── Step 1: geocode const geoUrl = 'https://geocoding.geo.census.gov/geocoder/locations/onelineaddress?' + new URLSearchParams({ address: address.trim(), benchmark: '2020', format: 'json', }); const geoResp = await fetch(geoUrl); if (!geoResp.ok) throw new Error('Geocoding service unavailable.'); const geoData = await geoResp.json(); const match = geoData?.result?.addressMatches?.[0]; if (!match) { setError('Address not found. Try a more specific street address (US only).'); setLoading(false); return; } lat = match.coordinates.y; lon = match.coordinates.x; matched = match.matchedAddress; // ── Step 2: USGS ASCE 7-16 design maps const usgsUrl = 'https://earthquake.usgs.gov/ws/designmaps/asce7-16.json?' + new URLSearchParams({ latitude: lat, longitude: lon, riskCategory: 'II', siteClass: 'D', title: 'SeisQuake', }); const usgsResp = await fetch(usgsUrl); if (!usgsResp.ok) throw new Error('USGS hazard service unavailable.'); const usgsData = await usgsResp.json(); r = usgsData?.response?.data; if (!r) throw new Error('No hazard data returned for this location.'); } } catch (err) { const isCors = err instanceof TypeError && /fetch/i.test(err.message); setError( isCors ? 'Live USGS / Census APIs are unreachable from this preview sandbox (CORS). On the deployed site at seisquake.com the widget queries the real services and returns real values — no fabricated data is ever shown to users.' : err.message || 'Lookup failed. Check the address and try again.' ); setLoading(false); return; } try { // Per ASCE 7-16, the MCEr PGA is PGAM (site-adjusted). PGA alone is // the uniform-hazard value before site coefficients. Prefer PGAM. const pgaMcer = r.pgam ?? r.pga ?? null; // True 10%/50yr PGA from USGS NSHM hazard-curve interpolation when // available (server-side). Fall back to MCEr ÷ 1.5 only if NSHM // didn't return — the widget surfaces which source is in use. const pga10in50Real = r.pga_10in50_real ?? null; const pga10in50 = pga10in50Real ?? (pgaMcer != null ? pgaMcer / 1.5 : null); const pga10in50IsApprox = pga10in50Real == null; // Fannie Mae's 0.15g trigger is defined against the 10%/50yr PGA, so // base the status on that — not on MCEr PGA — for accuracy. let status = 'green'; let statusLabel = 'Below trigger — SRA likely not required'; if (pga10in50 != null) { if (pga10in50 >= 0.35) { status = 'red'; statusLabel = 'SRA required — high seismic zone'; } else if (pga10in50 >= 0.15) { status = 'amber'; statusLabel = 'SRA required — Fannie Mae trigger met'; } } const next = { address: matched, lat, lon, pgaMcer, pga10in50, pga10in50IsApprox, ss: r.ss, s1: r.s1, sds: r.sds, sd1: r.sd1, // SDC can be null when Fv is undefined (ASCE 7-16 §11.4.8, common // in high-seismic zones). USGS provides the short-period SDC as // sdcs — use it as a fallback so the field is never empty. sdc: r.sdc || r.sdcs || null, pgam: r.pgam, status, statusLabel, }; setResult(next); try { localStorage.setItem(PGA_STORAGE_KEY, JSON.stringify({ input: address.trim(), result: next, ts: Date.now(), })); } catch (e) { /* localStorage may be unavailable */ } } catch (err) { setError(err.message || 'Lookup failed. Check the address and try again.'); } finally { setLoading(false); } }; const copyResult = () => { if (!result) return; const lines = [ `SeisQuake — Seismic Screening`, `Address: ${result.address}`, `Coords: ${result.lat.toFixed(5)}, ${result.lon.toFixed(5)}`, `MCEr PGA: ${fmt(result.pgaMcer)} g`, `~10%/50yr PGA: ${fmt(result.pga10in50)} g (MCEr ÷ 1.5)`, `Ss: ${fmt(result.ss)} · S1: ${fmt(result.s1)}`, `SDS: ${fmt(result.sds)} · SD1: ${fmt(result.sd1)}`, `SDC: ${result.sdc || '—'}`, `Fannie Mae Status: ${result.statusLabel}`, ].join('\n'); navigator.clipboard?.writeText(lines).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1800); }); }; const usgsLink = result ? `https://earthquake.usgs.gov/hazards/interactive/?latitude=${result.lat}&longitude=${result.lon}` : ''; return (
setAddress(e.target.value)} autoComplete="off" />
{loading && (
Geocoding address and pulling USGS ASCE 7-16 parameters…
)} {error &&
{error}
} {result && (
{result.statusLabel}
{result.address}
{result.lat.toFixed(5)}, {result.lon.toFixed(5)} · Risk Cat II · Site Class D
MCEr PGA
{fmt(result.pgaMcer)} g
~10%/50yr PGA{result.pga10in50IsApprox ? ' (approx)' : ''}
{fmt(result.pga10in50)} g
Ss
{fmt(result.ss)}
S1
{fmt(result.s1)}
SDS
{fmt(result.sds)}
SD1
{fmt(result.sd1)}
SDC
{result.sdc || '—'}
PGAM
{fmt(result.pgam)}
{result.pga10in50 != null && result.pga10in50 < 0.15 ? ( <>
Below Fannie Mae SRA trigger. Owner due diligence and portfolio pre-screening reports are available.
{ window.dispatchEvent( new CustomEvent('seisquake:prefill-order', { detail: { address: result.address, tier: 'Level 0' }, }) ); }} > Request Level 0 Screening → ) : ( <>
Above Fannie Mae SRA trigger. Lender submissions require a Level 1 SRA with Field Assessment.
{ window.dispatchEvent( new CustomEvent('seisquake:prefill-order', { detail: { address: result.address, tier: 'Level 1' }, }) ); }} > Request Level 1 Quote → )}
Open in USGS Tool ↗ { // Broadcast to the OrderSection so it can prefill the address field. window.dispatchEvent( new CustomEvent('seisquake:prefill-order', { detail: { address: result.address }, }) ); }} > Order Report for This Address →
Live data from the USGS National Seismic Hazard Maps. This preliminary screening takes seconds — your full PE-stamped ASTM E2026-24 report includes complete fault analysis, liquefaction susceptibility, SEL / SUL calculations, and lender-ready findings.{' '} Order a report →
)}
); } function fmt(n) { if (n == null || Number.isNaN(n)) return '—'; return Number(n).toFixed(3); } window.PgaWidget = PgaWidget;