DEV Community

Cover image for Aggregating 71M GPS-Tagged Grave Records Without Authentication
Crawler Bros
Crawler Bros

Posted on Fully Autonomous

Aggregating 71M GPS-Tagged Grave Records Without Authentication

Building a structured dataset of historical demographic or genealogical records often hits a wall at source interfaces. Searching for historical records across individual human names produces fragmented results, while attempting to dump entire regional cemeteries manually requires navigating heavy client-side rendering and arbitrary search relevance degradation.

BillionGraves hosts over 71 million crowd-sourced, GPS-linked grave photos alongside historical records such as the Social Security Death Index (SSDI) and U.S. Veteran Burial Records. While the website interface prompts for subscriptions on certain full-record detail pages, the site's underlying search endpoints return public record snippets without requiring logins, API keys, or cookies.

Extracting this data at scale requires a tool that can directly query these endpoints, handle search result pagination limits, normalize record fields, and handle geographic parameters reliably. The BillionGraves Scraper automates this process across ten distinct record collections.

Querying Record Collections and Handling Relevance Limits

The primary technical challenge when querying BillionGraves by name is that the underlying search infrastructure prioritizes broad relevance over strict boolean matching. If you request a common surname for a wide geographic area, the native API returns strong matches for the first few pages before silently broadening criteria to return loosely-related records.

To prevent emitting low-quality, out-of-spec records, the scraper re-verifies every record against your defined filters and halts execution early when consecutive pages yield no matching items.

The scraper supports three primary operational modes defined via the mode parameter:

  • search: Standard query targeting specific names, date ranges, geographic regions, or record collections.
  • byCemetery: Complete enumeration of all photographed records indexed within a single cemetery.
  • searchCemeteries: Geographic and metadata search to look up specific cemeteries and retrieve their underlying cemeteryId.

For standard record searches, you filter by names using familyNames, givenNames, and maidenNames. Date constraints are passed using integer bounds like birthYearMin, birthYearMax, deathYearMin, and deathYearMax.

When querying specific datasets, such as military histories, setting the collectionId limits the payload. Collection 1 targets GPS Headstones, while Collection 2 targets Veteran Burial Records, which expose additional schema properties like militaryBranch, militaryRank, militaryConflict, and militaryUnit.

{
  "mode": "search",
  "familyNames": "Smith",
  "collectionId": "2",
  "state": "Texas",
  "birthYearMin": 1890,
  "birthYearMax": 1910,
  "maxItems": 100
}
Enter fullscreen mode Exit fullscreen mode

When targeting a specific burial ground, querying by surname is inefficient. Instead, you first resolve the cemetery's system identifier using searchCemeteries with the cemeteryNameQuery parameter:

{
  "mode": "searchCemeteries",
  "cemeteryNameQuery": "Fort Sam Houston",
  "state": "Texas",
  "maxItems": 10
}
Enter fullscreen mode Exit fullscreen mode

This returns cemetery objects containing the exact integer cemeteryId, total record counts, precise latitude and longitude, and address details.

How to Set Up a Scrape Run

Extracting a structured collection from BillionGraves follows a four-step execution flow:

  1. Identify the Target Collection or Location: Decide whether you are extracting records by surname across regions, or enumerating a single physical location. If enumerating a location, run the actor in searchCemeteries mode first to fetch the target cemeteryId.
  2. Configure Input Parameters: Construct your JSON payload. Specify the mode, strict date bounds (birthYearMin, deathYearMin), and geographic filters (state, country).
  3. Execute the Actor: Pass the input payload to the actor. The task queries BillionGraves' public backend directly, bypassing client-side web page execution entirely.
  4. Collect Standardized Dataset Items: The output dataset populates directly with clean record objects. Any empty attributes on a record are automatically omitted from the JSON payload rather than set to null.

Here is an example execution payload browsing a specific cemetery using its cemeteryId in byCemetery mode:

{
  "mode": "byCemetery",
  "cemeteryId": 107742,
  "cemeteryName": "Kaysville City Cemetery",
  "maxItems": 500
}
Enter fullscreen mode Exit fullscreen mode

Dataset Structure and Record Attributes

The output schema varies depending on whether the actor runs in a grave/person search mode (search, byCemetery) or a cemetery discovery mode (searchCemeteries).

A representative grave record from Collection 1 (GPS Headstones) returns spatial coordinates alongside personal information:

{
  "recordId": "1849201",
  "sourceUrl": "https://billiongraves.com/grave/John-Smith/1849201",
  "thumbnailUrl": "https://s3.amazonaws.com/bg-images/...",
  "fullName": "John Smith",
  "givenNames": "John",
  "familyNames": "Smith",
  "birthDate": "1862-04-12",
  "deathDate": "1931-11-05",
  "cemeteryId": 107742,
  "cemeteryName": "Kaysville City Cemetery",
  "city": "Kaysville",
  "county": "Davis",
  "state": "Utah",
  "country": "United States",
  "latitude": 41.034511,
  "longitude": -111.938822,
  "collectionId": "1",
  "collectionTitle": "GPS Headstones",
  "recordType": "grave",
  "scrapedAt": "2026-03-30T10:15:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

Latitude and longitude fields appear specifically on headstone records photographed via the volunteer app. Document-based collections (such as SSDI) omit spatial attributes because they lack physical GPS tags.

Pricing Structure

This actor uses a flat event-based pricing model. You are charged strictly per dataset item emitted and per actor startup instance:

  • Actor Start (apify-actor-start): $0.005 per GB of memory allocated to the run, charged once when the run begins.
  • Dataset Results (apify-default-dataset-item): $0.005 per result emitted to the default dataset at the default FREE tier.

Volume tier pricing automatically lowers the per-result cost for higher platform usage tiers:

  • FREE: $0.005 per result
  • BRONZE: $0.00433 per result
  • SILVER: $0.00367 per result
  • GOLD / PLATINUM / DIAMOND: $0.003 per result

Running a search job that returns 200 validated grave records on the default tier incurs $0.005 for the single actor start (assuming 1 GB memory allocation) plus 200 results at $0.005 each ($1.00), bringing the total run cost to $1.005.

Edge Cases and Limitations

This scraper operates against live search indexes, which introduces specific boundary conditions you must account for in downstream ingestion pipelines.

BillionGraves' backend search index periodically retains references to records whose underlying volunteer photos have been deleted or set to private on the live site. As a result, roughly 5% to 20% of generated sourceUrl or thumbnailUrl links on common-name searches may return HTTP 404 or 410 status codes when requested directly. The tabular name, date, location, and coordinate fields on the dataset record itself remain accurate, but downstream image fetchers must handle broken media URLs gracefully.

Additionally, this approach is not designed for continuous bulk-scraping of massive national cemeteries exceeding 100,000 records. BillionGraves' backend aggregation queries on extremely large cemeteries frequently take over 60 seconds to paginate, causing their upstream servers to issue HTTP 504 Gateway Timeout errors regardless of request retries.


Runs in this article used BillionGraves Scraper. Its README is the reference for input fields and output structure; this post is only one path through them.

Top comments (0)