<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Groundplane Labs]]></title><description><![CDATA[Groundplane Labs]]></description><link>https://groundplanes.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 09:41:47 GMT</lastBuildDate><atom:link href="https://groundplanes.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Turning WARN Act layoff notices into a weekly dataset (CA, NY, OR, WA)]]></title><description><![CDATA[When a large employer plans a plant closing or mass layoff in the US, the federal WARN Act generally requires advance written notice, usually 60 days, and…]]></description><link>https://groundplanes.hashnode.dev/turning-warn-act-layoff-notices-into-a-weekly-dataset-ca-ny-or-wa</link><guid isPermaLink="true">https://groundplanes.hashnode.dev/turning-warn-act-layoff-notices-into-a-weekly-dataset-ca-ny-or-wa</guid><category><![CDATA[open data]]></category><category><![CDATA[Python]]></category><category><![CDATA[data]]></category><category><![CDATA[api]]></category><category><![CDATA[recruiting]]></category><dc:creator><![CDATA[Groundplane Labs]]></dc:creator><pubDate>Sun, 25 Oct 2026 17:18:14 GMT</pubDate><content:encoded><![CDATA[<p>When a large employer plans a plant closing or mass layoff in the US, the federal WARN Act generally requires advance written notice, usually 60 days, and states publish those notices. Some states go further: California's and New York's own WARN laws cover smaller employers, and New York asks for 90 days' notice instead of 60.</p>
<p>That makes WARN notices one of the earliest public signals that a company is shrinking a site. Recruiters use them to reach people before their last day, workforce boards use them to plan rapid-response sessions, and analysts use them to track a region or industry.</p>
<p>The data is public, but every state publishes it in a different place and shape. Disclosure: I built and sell a scraper that normalizes four of them on Apify. This post was drafted with AI help and reviewed by me.</p>
<h2>What varies between states</h2>
<ul>
<li><strong>Format.</strong> One state gives you a spreadsheet, another an HTML table, another a page per notice. Column names differ, and so do date formats.</li>
<li><strong>Dates.</strong> You usually get two: when the notice was received and when the layoff takes effect. Some rows have a range or "TBD" for the effective date, so treat it as text first and parse it second.</li>
<li><strong>Layoff vs closure.</strong> Some states mark this explicitly; others only say it in free text. If you care about full closures, keep the raw value next to your normalized one.</li>
<li><strong>Updates.</strong> Employers amend notices (new dates, different headcount). Expect the same company and site to appear more than once, and dedupe on company + location + notice date rather than company name alone.</li>
<li><strong>Company names.</strong> The legal entity on the notice often isn't the brand people know. Matching to a CRM needs fuzzy matching or a manual pass.</li>
</ul>
<h2>A simple weekly workflow</h2>
<ol>
<li>Pull the last 7 days of notices for the states you care about.</li>
<li>Filter by employees affected (for example 50+) so you're not chasing single-digit notices.</li>
<li>Group by company and county to spot repeat filers.</li>
<li>Push the result to a sheet or Slack once a week.</li>
</ol>
<p>If you'd rather not maintain four parsers, the actor I sell does steps 1 and 2 and returns one JSON shape for all four states: company, location, employees affected, notice and effective dates, and layoff or closure, newest first:
<a href="https://apify.com/groundplanes/warn-act-layoff-notices">WARN Act Layoff &amp; Plant Closure Notices Scraper</a></p>
<h2>A note on use</h2>
<p>These are notices about workplaces, not individuals, and they are published so communities can respond. Good uses include recruiting, workforce planning and market research. If you're contacting affected employees, lead with something useful to them.</p>
<h2>Related public-records feeds I maintain</h2>
<ul>
<li><a href="https://apify.com/groundplanes/courtlistener-bankruptcy-dockets">Chapter 11 bankruptcy dockets from CourtListener</a>, business debtors only</li>
<li><a href="https://apify.com/groundplanes/gsa-auctions-surplus-lots">GSA Auctions federal surplus lots</a></li>
</ul>
<p>If one of these returns partial or odd results, please open an issue on the actor's Issues tab on Apify so I can fix it.</p>
]]></content:encoded></item><item><title><![CDATA[Pulling restaurant health inspection data via API (9 US jurisdictions, JSON out)]]></title><description><![CDATA[Restaurant health inspection results are public almost everywhere in the US, but every county publishes them differently: Socrata APIs, ArcGIS layers, CSV…]]></description><link>https://groundplanes.hashnode.dev/pulling-restaurant-health-inspection-data-via-api-9-us-jurisdictions-json-out</link><guid isPermaLink="true">https://groundplanes.hashnode.dev/pulling-restaurant-health-inspection-data-via-api-9-us-jurisdictions-json-out</guid><category><![CDATA[api]]></category><category><![CDATA[#OpenData]]></category><category><![CDATA[Python]]></category><category><![CDATA[datascience]]></category><dc:creator><![CDATA[Groundplane Labs]]></dc:creator><pubDate>Mon, 19 Oct 2026 17:20:37 GMT</pubDate><content:encoded><![CDATA[<p>Restaurant health inspection results are public almost everywhere in the US, but every county publishes them differently: Socrata APIs, ArcGIS layers, CSV dumps, or a search form that only shows one permit at a time. If you're building a food-safety dashboard, a restaurant review app, or a lead list for kitchen-equipment or pest-control sales, the hard part is the plumbing, not the analysis.</p>
<p>I maintain a set of Apify actors that normalize this data for 9 jurisdictions: Maricopa County (Phoenix), Las Vegas (SNHD), Allegheny County (Pittsburgh), Delaware, Boulder County, Montgomery County MD, New York State (non-NYC), Boston, and Santa Clara County (San Jose). Disclosure: I built and sell these, and this post was drafted with AI help and reviewed by me.</p>
<h2>What the output looks like</h2>
<p>Real rows from a recent Maricopa County run (ZIP 85281, Tempe), trimmed:</p>
<pre><code class="language-json">{
  "permitId": "FD-00616",
  "businessName": "Denmart",
  "permitType": "Food Establishment - Retail Food",
  "address": "809 S RURAL RD TEMPE AZ 85281",
  "inspectionCount": 6,
  "inspections": [
    {"date": "2026-09-21", "purpose": "Food Routine Inspection", "grade": "A",
     "priorityViolations": 0,
     "reportUrl": "https://envapp.maricopa.gov/Inspection/FD-00616/INSP-092838-2026"}
  ]
}
</code></pre>
<p>Maricopa only exposes a per-permit search page, so the actor walks permits by ZIP and nests each permit's inspection history. The Socrata-backed cities (Boston, Montgomery, etc.) return one row per violation, with an option to collapse to one row per inspection.</p>
<h2>Calling it from Python</h2>
<pre><code class="language-python">from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("groundplanes/maricopa-restaurant-inspections").call(
    run_input={"zipCodes": ["85281"], "maxPermits": 50}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    latest = item["inspections"][0] if item["inspections"] else None
    print(item["businessName"], latest and latest["grade"])
</code></pre>
<p>(Check each actor's input tab for the exact field names; they differ by source.)</p>
<h2>Things I learned normalizing 9 sources</h2>
<ul>
<li><strong>Grades aren't comparable across counties.</strong> Maricopa uses A–D plus "Cutting Edge" participation; Las Vegas uses A/B/C with demerits; Boston uses pass/fail codes that changed in 2018. Keep the raw value next to any normalized one.</li>
<li><strong>IDs get reassigned.</strong> Boston's open-data <code>_id</code> changes on the daily reload, so dedupe on license + inspection date, not the row ID.</li>
<li><strong>Undated and administrative rows exist.</strong> Boston has ~11k "HE_NotReq" stamps that look like clean inspections but aren't.</li>
<li><strong>ZIPs are messy.</strong> ZIP+4 and 4-digit ZIPs (leading zero dropped) show up; match on the first 5 characters after zero-padding.</li>
</ul>
<h2>Price</h2>
<p>Pay per result: $3 per 1,000 results ($0.003 each) plus a $0.00005 start fee. Apify's free plan includes $5/month of credit, so roughly 1,600 results a month cost you nothing. Paid Apify plans get 10–30% off.</p>
<h2>Links</h2>
<ul>
<li>Maricopa: <a href="https://apify.com/groundplanes/maricopa-restaurant-inspections">https://apify.com/groundplanes/maricopa-restaurant-inspections</a></li>
<li>Las Vegas: <a href="https://apify.com/groundplanes/las-vegas-restaurant-inspections">https://apify.com/groundplanes/las-vegas-restaurant-inspections</a></li>
<li>Pittsburgh/Allegheny: <a href="https://apify.com/groundplanes/allegheny-restaurant-inspections">https://apify.com/groundplanes/allegheny-restaurant-inspections</a></li>
<li>Boston: <a href="https://apify.com/groundplanes/boston-food-establishment-inspections">https://apify.com/groundplanes/boston-food-establishment-inspections</a></li>
<li>Santa Clara: <a href="https://apify.com/groundplanes/santa-clara-county-restaurant-inspections">https://apify.com/groundplanes/santa-clara-county-restaurant-inspections</a></li>
<li>NY State: <a href="https://apify.com/groundplanes/ny-state-food-service-inspections">https://apify.com/groundplanes/ny-state-food-service-inspections</a></li>
<li>Montgomery MD: <a href="https://apify.com/groundplanes/montgomery-md-food-inspections">https://apify.com/groundplanes/montgomery-md-food-inspections</a></li>
<li>Delaware: <a href="https://apify.com/groundplanes/delaware-restaurant-inspections">https://apify.com/groundplanes/delaware-restaurant-inspections</a></li>
<li>Boulder County: <a href="https://apify.com/groundplanes/boulder-county-restaurant-inspections">https://apify.com/groundplanes/boulder-county-restaurant-inspections</a></li>
</ul>
<p>If there's a county you need that isn't here, say so in the comments. That's how I pick the next one.</p>
]]></content:encoded></item><item><title><![CDATA[Finding distressed-property leads from city code-violation data]]></title><description><![CDATA[Most "driving for dollars" lists are built by literally driving around looking for overgrown yards and boarded windows. Cities already do that driving for…]]></description><link>https://groundplanes.hashnode.dev/finding-distressed-property-leads-from-city-code-violation-data</link><guid isPermaLink="true">https://groundplanes.hashnode.dev/finding-distressed-property-leads-from-city-code-violation-data</guid><category><![CDATA[Real Estate Investing]]></category><category><![CDATA[Real Estate]]></category><category><![CDATA[open data]]></category><category><![CDATA[lead generation]]></category><category><![CDATA[Wholesaling]]></category><dc:creator><![CDATA[Groundplane Labs]]></dc:creator><pubDate>Mon, 19 Oct 2026 17:20:14 GMT</pubDate><content:encoded><![CDATA[<p>Most "driving for dollars" lists are built by literally driving around looking for overgrown yards and boarded windows. Cities already do that driving for you. Code inspectors log every complaint, inspection and court referral, and many cities publish those logs as open data.</p>
<p>Disclosure up front: I build and sell small tools that pull this data, linked at the bottom. This piece was drafted with AI help and checked by me. The method works with or without my tools.</p>
<h2>Why code violations are a useful signal</h2>
<p>A single weeds citation means little. Patterns mean more:</p>
<ul>
<li><strong>Repeat cases at one address</strong> over months: the owner isn't fixing things.</li>
<li><strong>Cases escalated to court</strong> ("In Court", "Filing Court Paperwork"): the owner has ignored notices.</li>
<li><strong>Vacant/structure cases</strong> alongside refuse or weeds: possible abandonment.</li>
<li><strong>Out-of-area mailing addresses</strong> (when you join to the county assessor by parcel ID): absentee owner.</li>
</ul>
<p>None of these means an owner wants to sell. They're a reason to look closer and send a respectful letter, not a guarantee.</p>
<h2>What the data actually looks like</h2>
<p>Three real rows from Pittsburgh's code-enforcement feed (via WPRDC), from inspections on Oct 16, 2026, trimmed:</p>
<table>
<thead>
<tr>
<th>Case</th>
<th>Address</th>
<th>Type</th>
<th>Status</th>
<th>Outcome</th>
<th>Neighborhood</th>
</tr>
</thead>
<tbody><tr>
<td>CF-ES-2026-066164</td>
<td>1546 Broadhead St, 15206</td>
<td>Refuse Violations</td>
<td>In Court</td>
<td>Filing Court Paperwork</td>
<td>Lincoln-Lemington-Belmar</td>
</tr>
<tr>
<td>CF-ES-2026-069651</td>
<td>6955 Lemington Ave, 15206</td>
<td>Weeds and Debris</td>
<td>In Violation</td>
<td>Violation Found</td>
<td>Lincoln-Lemington-Belmar</td>
</tr>
<tr>
<td>CF-ES-2026-063966</td>
<td>1304 Westfield St, 15216</td>
<td>Refuse Violations</td>
<td>Ready to Close</td>
<td>Violation Resolved</td>
<td>Beechview</td>
</tr>
</tbody></table>
<p>Each row also carries the parcel ID, latitude/longitude, ward and council district. The parcel ID is the key: join it to the Allegheny County assessment data to get owner name and mailing address.</p>
<p>The first row is the interesting one: still in violation and headed to court. The third was fixed, so you'd filter it out.</p>
<h2>A simple workflow</h2>
<ol>
<li>Pull the last 6–12 months of cases for your target city.</li>
<li>Group by address/parcel; keep addresses with 2+ cases or any court status.</li>
<li>Join to assessor data for owner and mailing address; flag absentee owners.</li>
<li>Drive by or check street view before you mail anything.</li>
<li>Follow your state's rules on solicitation, and don't misrepresent yourself as the city.</li>
</ol>
<h2>Cities I cover</h2>
<ul>
<li><strong>Pittsburgh</strong>, with court status and parcel IDs: <a href="https://apify.com/groundplanes/pittsburgh-code-violations">https://apify.com/groundplanes/pittsburgh-code-violations</a></li>
<li><strong>Orlando, FL</strong>, with case types and map coordinates for recent cases: <a href="https://apify.com/groundplanes/orlando-code-enforcement-cases">https://apify.com/groundplanes/orlando-code-enforcement-cases</a></li>
<li><strong>New Orleans</strong>, with an exact violation count per case: <a href="https://apify.com/groundplanes/nola-code-enforcement-violations">https://apify.com/groundplanes/nola-code-enforcement-violations</a></li>
</ul>
<h2>Cost</h2>
<p>Pittsburgh is $4 per 1,000 records; Orlando and New Orleans are $3 per 1,000. Apify gives free accounts $5 of credit a month, so a few months of one city's cases usually costs nothing. For example, a month of New Orleans violations (~1,600 rows) is about $4.80, or about $1.20 if you collapse it to one row per case.</p>
<p>If your market isn't listed, tell me which city. I add cities based on what people ask for.</p>
]]></content:encoded></item><item><title><![CDATA[Fresh new-business leads from city license filings (and the compliance traps)]]></title><description><![CDATA[When a business registers for a city license, the filing often becomes public within days: name, owner, business type, address and start date. For anyone…]]></description><link>https://groundplanes.hashnode.dev/fresh-new-business-leads-from-city-license-filings-and-the-compliance-traps</link><guid isPermaLink="true">https://groundplanes.hashnode.dev/fresh-new-business-leads-from-city-license-filings-and-the-compliance-traps</guid><category><![CDATA[#OpenData]]></category><category><![CDATA[Python]]></category><category><![CDATA[lead generation]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Groundplane Labs]]></dc:creator><pubDate>Mon, 19 Oct 2026 17:14:31 GMT</pubDate><content:encoded><![CDATA[<p>When a business registers for a city license, the filing often becomes public within days: name, owner, business type, address and start date. For anyone selling to brand-new businesses (POS systems, insurance, bookkeeping, signage, payroll), that's the freshest lead source there is, and it's free at the source.</p>
<p>The catch is that every city publishes it differently. I maintain normalizers for four of them on Apify: New Orleans, Delaware (statewide), Norfolk VA, and Berkeley CA. Disclosure: I built and sell these. This post was drafted with AI help and reviewed by me.</p>
<h2>Real rows</h2>
<p>New Orleans occupational licenses, from a recent run, trimmed:</p>
<pre><code class="language-json">[
  {"businessName": "DJINN LOGIC", "ownerName": "DJINN LOGIC LLC DBA DJINN LOGI",
   "businessType": "Scientific &amp; Technical Consulting Services, Other",
   "licenseNumber": "106009944", "startDate": "2027-01-04",
   "address": "1928 VALENCE ST", "zip": "70115", "startsInFuture": true,
   "latitude": 29.9299, "longitude": -90.1061},
  {"businessName": null, "ownerName": "JOHN BOSS COLLECTION LLC",
   "businessType": "Graphic Design Services", "licenseNumber": "105070793",
   "startDate": "2027-02-27", "address": "6017 FRANKLIN AVE", "zip": "70122",
   "startsInFuture": true}
]
</code></pre>
<p><code>startsInFuture</code> flags licenses with a start date that hasn't arrived yet. Most of those dated in the next few months look like planned openings, which are the earliest leads you can get. A few years-out dates are probably typos, so check them.</p>
<h2>Data-quality lessons</h2>
<ul>
<li><strong>Missing coordinates must be null, not 0.0.</strong> Otherwise your map shows thousands of businesses in the Gulf of Guinea.</li>
<li><strong>"Duplicates" usually aren't.</strong> In New Orleans, most repeated license numbers have different mailing addresses, so we keep one row per license and put every mailing address in <code>allMailingAddresses</code>.</li>
<li><strong>Phone fields are dirty.</strong> Extensions ("x124", "EXT6"), placeholders like (999) 999-9999, and even email fragments turn up. The actor splits out <code>phoneExtension</code>, rejects invalid NANP numbers, and outputs <code>phoneE164</code>.</li>
</ul>
<h2>The compliance part (read this before you dial)</h2>
<p>Phone output is <strong>off by default</strong> (<code>includePhone</code>). Many license holders are sole proprietors, and their number is a personal cell. In the US:</p>
<ul>
<li>The TCPA requires prior express consent for autodialed or prerecorded calls and marketing texts to cell phones.</li>
<li>The National Do Not Call registry doesn't exempt sole-proprietor residential or cell lines.</li>
<li>Louisiana requires telephone solicitors to register with the LA PSC.</li>
</ul>
<p>Mailers and in-person visits avoid most of this.</p>
<h2>Python</h2>
<pre><code class="language-python">from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("groundplanes/nola-occupational-business-licenses").call(run_input={"maxItems": 100})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["startDate"], row["businessType"], row["address"])
</code></pre>
<p>(See each actor's input tab for filters such as date range and business type.)</p>
<h2>Price and links</h2>
<ul>
<li>New Orleans, $3 per 1,000: <a href="https://apify.com/groundplanes/nola-occupational-business-licenses">https://apify.com/groundplanes/nola-occupational-business-licenses</a></li>
<li>Norfolk VA, $3 per 1,000: <a href="https://apify.com/groundplanes/norfolk-va-business-licenses">https://apify.com/groundplanes/norfolk-va-business-licenses</a></li>
<li>Berkeley CA, $3 per 1,000: <a href="https://apify.com/groundplanes/berkeley-ca-business-licenses">https://apify.com/groundplanes/berkeley-ca-business-licenses</a></li>
<li>Delaware, $5 per 1,000: <a href="https://apify.com/groundplanes/delaware-business-licenses">https://apify.com/groundplanes/delaware-business-licenses</a></li>
</ul>
<p>Every actor also charges a $0.00005 start fee. Apify's free plan includes $5 of monthly credit, which works out to about 1,600 rows at $3 per 1,000. Paid Apify plans get 10–30% off.</p>
<p>Which city should be next? Tell me in the comments.</p>
]]></content:encoded></item></channel></rss>