Skip to main content

Overview

The permit data integration fetches construction permits from two NYC Department of Buildings (DOB) datasets and merges them into a unified stream. This handles both work-type permits (General Construction, Plumbing, etc.) and job-level filings (New Building, Demolition).
Two Implementations: The project has two permit fetching implementations:
  • permits.ts - Used by the main isometric view (App component), takes daysBack: number
  • permit-data.ts - Used by the alternative map view (PermitMap component), takes explicit date strings
This page documents the permit-data.ts module used by PermitMap. For the isometric view’s implementation, see permits.ts.

fetchPermits()

Fetches permits for a specific date range by querying two DOB NOW datasets and merging the results.
src/permit-data.ts
export async function fetchPermits(
  dateFrom: string,
  dateTo: string,
  limit: number = 2000
): Promise<Permit[]>

Parameters

dateFrom
string
required
Start date in YYYY-MM-DD format (e.g., '2024-01-01')
dateTo
string
required
End date in YYYY-MM-DD format (e.g., '2024-01-07'). Range is inclusive.
limit
number
default:2000
Maximum number of permits to fetch. Used to cap map markers for performance.

Returns

Returns a Promise<Permit[]> containing normalized permits from both datasets.

Implementation Details

Two-Dataset Strategy

The function queries two separate Socrata datasets:
  1. rbx6-tga4 — DOB NOW: Build – Approved Permits
    Work-type permits: General Construction, Plumbing, Mechanical, Solar, Scaffold, etc.
    Excludes New Building and Full Demolition.
  2. w9ak-ipjd — DOB NOW: Build – Job Filings
    Job-level filings: New Building, Full Demolition, Alteration.
    Only NB and DM records are pulled from this dataset.

Proxy Paths

All requests use proxy paths to avoid CORS issues:
src/permit-data.ts
const PERMITS_BASE = '/api/permits';  // Proxies to data.cityofnewyork.us/resource/rbx6-tga4.json
const JOBS_BASE = '/api/jobs';        // Proxies to data.cityofnewyork.us/resource/w9ak-ipjd.json
Vite handles these in development; Vercel rewrites handle them in production.

Query String Construction

Do NOT use URLSearchParams — it double-encodes $ as %24, breaking Socrata’s API which requires literal $order, $where, etc.
Query strings are manually constructed using Socrata Query Language (SoQL):
src/permit-data.ts
const workQuery = [
  `$order=issued_date DESC`,
  `$limit=${limit}`,
  `$where=issued_date >= '${cutoffStr}' AND latitude IS NOT NULL AND longitude IS NOT NULL`,
].map(p => p.replace(/ /g, '+')).join('&');
Spaces are manually replaced with + for URL encoding.

Date Cutoff Calculation

The function uses getLatestDatasetDate() to determine the actual latest available data, rather than assuming the dataset is current:
src/permit-data.ts
const latestDate = await getLatestDatasetDate();
const cutoff = new Date(latestDate);
cutoff.setDate(cutoff.getDate() - (daysBack - 1));
const cutoffStr = cutoff.toISOString().split('T')[0];  // "2026-02-03"
This accounts for datasets that may lag 2-3 days behind real-time.

Parallel Fetch

Both datasets are fetched in parallel for performance:
src/permit-data.ts
const [workRes, jobRes] = await Promise.all([
  fetch(`${PERMITS_BASE}?${workQuery}`, { cache: 'no-store' }),
  fetch(`${JOBS_BASE}?${jobQuery}`, { cache: 'no-store' }),
]);

Response Normalization

Work permits and job filings have different schemas and must be normalized: Work Permits:
src/permit-data.ts
const workPermits = workRaw.map(p => ({
  ...p,
  job_type: workTypeToCode(p.work_type ?? ''),  // "General Construction" → "GC"
}));
Job Filings:
src/permit-data.ts
const jobPermits: Permit[] = jobRaw.map(p => ({
  ...p,
  work_type: p.job_type,                         // "New Building" / "Full Demolition"
  job_type: p.job_type === 'New Building' ? 'NB' : 'DM',
  issued_date: p.approved_date,                  // Use approved_date as issued date
}));

Final Merge

src/permit-data.ts
return [...workPermits, ...jobPermits];

Example Request

const permits = await fetchPermits(7);  // Last 7 days
console.log(`Fetched ${permits.length} permits`);

Error Handling

src/permit-data.ts
if (!workRes.ok) throw new Error(`Permits API error: ${workRes.status}`);
if (!jobRes.ok) throw new Error(`Jobs API error: ${jobRes.status}`);
Throws errors for non-200 responses. Calling code should handle with try/catch.

getLatestDatasetDate()

Determines the actual latest available date in the permits dataset, accounting for data lag.
src/permit-data.ts
async function getLatestDatasetDate(): Promise<Date>

Caching Strategy

The latest dataset date is cached for 10 minutes to avoid redundant API calls on every filter change:
src/permit-data.ts
let _latestDateCache: { date: string; fetchedAt: number } | null = null;

async function getLatestDatasetDate(): Promise<Date> {
  const now = Date.now();
  // Cache for 10 minutes
  if (_latestDateCache && now - _latestDateCache.fetchedAt < 10 * 60 * 1000) {
    return new Date(_latestDateCache.date);
  }
  // ... fetch logic
}

Query Implementation

Uses SoQL’s max() aggregation function:
src/permit-data.ts
const res = await fetch(`${PERMITS_BASE}?$select=max(issued_date)`);
const data = await res.json();
const dateStr = data[0]?.max_issued_date ?? data[0]?.max_issued_date;

Fallback Logic

If the API call fails or returns no data, falls back to 2 days ago:
src/permit-data.ts
const d = new Date();
d.setDate(d.getDate() - 2);
return d;
The 2-day fallback accounts for typical DOB dataset lag. Real-world observation shows datasets are usually 2-3 days behind real-time.

workTypeToCode()

Normalizes verbose work type strings to two-letter codes.
src/permit-data.ts
export function workTypeToCode(workType: string): string

Supported Mappings

Work TypeCode
New BuildingNB
Full DemolitionDM
General ConstructionGC
PlumbingPL
MechanicalME
SolarSOL
Sidewalk ShedSHD
ScaffoldSCF
Construction FenceFNC
SignSG
FoundationFND
StructuralSTR
BoilerBLR
SprinklerSPR
Earth WorkEW
AntennaANT
Curb CutCC
StandpipeSTP
OtherOTH

Implementation

src/permit-data.ts
export function workTypeToCode(workType: string): string {
  const wt = workType.toLowerCase();
  if (wt.includes('new building'))      return 'NB';
  if (wt.includes('full demolition'))   return 'DM';
  if (wt.includes('general construction')) return 'GC';
  // ... 15 more conditions
  return 'OTH';
}
Uses substring matching (case-insensitive) to handle variations in dataset formatting.

Type Definitions

Permit Interface

src/types.ts
export interface Permit {
  // Identity
  job_filing_number?: string;
  work_permit?: string;
  tracking_number?: string;
  bin?: string;
  sequence_number?: string;

  // Address
  house_no?: string;
  street_name?: string;
  borough?: string;
  zip_code?: string;
  block?: string;
  lot?: string;
  bbl?: string;
  community_board?: string;
  nta?: string;               // Neighborhood name (e.g. "Fort Greene")
  council_district?: string;

  // Work
  work_type?: string;         // Verbose: "General Construction", "Full Demolition"
  job_type?: string;          // Normalized code: NB/DM/GC/etc.
  permit_status?: string;
  job_description?: string;
  filing_reason?: string;
  work_on_floor?: string;
  estimated_job_costs?: string;

  // Dates (ISO format: 2026-02-24T00:00:00.000)
  issued_date?: string;
  approved_date?: string;
  expired_date?: string;

  // Owner
  owner_name?: string;
  owner_business_name?: string;

  // Applicant / contractor
  applicant_first_name?: string;
  applicant_last_name?: string;
  applicant_business_name?: string;
  applicant_business_address?: string;

  // Filing representative (expediter)
  filing_representative_first_name?: string;
  filing_representative_last_name?: string;
  filing_representative_business_name?: string;

  // Coordinates
  latitude?: string;
  longitude?: string;
}

See Utilities for helper functions:
  • formatAddress(permit) — Formats permit address
  • formatDate(dateStr) — Formats ISO date strings
  • getJobColor(jobType) — Returns hex color for job type
  • getJobEmoji(jobType) — Returns emoji for job type
  • getJobLabel(jobType) — Returns human-readable label

Build docs developers (and LLMs) love