← back to AbramsOS

lib/neighborhood.js

69 lines

// neighborhood.js — Neighborhood Watch link builder + free geocoder.
//
// Given an address (+ optional radius in miles), geocode it via OpenStreetMap Nominatim
// (free, no API key) and build real deep-links into the community-safety apps: Ring
// (cameras + Neighbors), Nextdoor, Citizen, crime maps, and the sex-offender registries.
// No data is fabricated — every link is a real public URL; location-aware ones carry the
// geocoded lat/lon when we have it, otherwise the raw address query.

async function geocode(address) {
  if (!address) return null;
  const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(address)}&format=json&limit=1&addressdetails=1`;
  try {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), 8000);
    const r = await fetch(url, { headers: { 'User-Agent': 'AbramsOS/1.0 (personal household admin)' }, signal: ctrl.signal });
    clearTimeout(t);
    if (!r.ok) return null;
    const j = await r.json();
    if (!Array.isArray(j) || !j.length) return null;
    const hit = j[0];
    return {
      lat: Number(hit.lat), lon: Number(hit.lon),
      displayName: hit.display_name,
      city: (hit.address && (hit.address.city || hit.address.town || hit.address.village || hit.address.county)) || null,
      state: (hit.address && hit.address.state) || null,
    };
  } catch (_) { return null; }
}

// Build the categorized link set. loc may be null (links degrade to address-query form).
function links({ address, loc, radiusMi = 2 }) {
  const lat = loc && loc.lat, lon = loc && loc.lon;
  const hasGeo = Number.isFinite(lat) && Number.isFinite(lon);
  const q = encodeURIComponent(address || '');
  const city = (loc && (loc.city || loc.state)) ? encodeURIComponent(`${loc.city || ''} ${loc.state || ''}`.trim()) : q;

  const cat = (title, items) => ({ title, items: items.filter(Boolean) });
  const L = (name, url, note) => ({ name, url, note: note || null });

  return [
    cat('Your Ring', [
      L('Ring cameras (live view)', 'https://account.ring.com/account/dashboard', 'Your own Ring doorbell/cameras — sign in to view live & history'),
      L('Ring Neighbors feed', 'https://neighbors.ring.com/', 'Local safety posts & shared camera clips near you'),
    ]),
    cat('Community apps', [
      L('Nextdoor', 'https://nextdoor.com/news_feed/', 'Your registered neighborhood feed'),
      L('Citizen', 'https://citizen.com/', 'Real-time local safety alerts (app-based)'),
      L('Amber Alerts', 'https://www.missingkids.org/gethelpnow/amber', 'Active AMBER alerts'),
    ]),
    cat(`Crime near here (~${radiusMi} mi)`, [
      hasGeo
        ? L('SpotCrime map', `https://spotcrime.com/map?lat=${lat}&lon=${lon}&zoom=15`, 'Recent reported crime on a map')
        : L('SpotCrime', `https://spotcrime.com/`, 'Recent reported crime'),
      L('CrimeMapping', address ? `https://www.crimemapping.com/map/location/${q}` : 'https://www.crimemapping.com/', 'Police-sourced incident map'),
      L('FBI Crime Data Explorer', 'https://cde.ucr.cjis.gov/', 'Official agency crime statistics'),
    ]),
    cat('Registries & local', [
      L('Sex-offender registry (national)', hasGeo ? `https://www.nsopw.gov/en/Search/Verification` : 'https://www.nsopw.gov/', 'US DOJ NSOPW — search by address'),
      L("CA Megan's Law", 'https://www.meganslaw.ca.gov/', 'California registered-offender map'),
      L('Local police (non-emergency)', `https://www.google.com/search?q=${city}+police+non-emergency+number`, 'Find your local department'),
    ]),
    cat('Map', [
      hasGeo ? L('Open in OpenStreetMap', `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=15/${lat}/${lon}`, null) : null,
    ]),
  ].filter((c) => c.items.length);
}

module.exports = { geocode, links };