Skip to content

How I Built AEDLocator.sg From Singapore's Public AED Data

How I Built AEDLocator.sg From Singapore's Public AED Data

Singapore already publishes public-access AED locations. The GeoJSON includes building names, postal codes, coordinates, floor levels, operating hours, and notes like "Level 1 outside management office."

That still does not answer the question people actually ask: is there an AED in this mall, this school, this HDB block, and can I get to it after 6pm?

That gap is why I built AEDLocator.sg. I wanted the public dataset to behave like a directory you can browse before anyone needs it, not a file you only open after something has gone wrong.

If someone collapses, call 995 immediately. Use SCDF's myResponder app for live emergency response. AEDLocator.sg is a preparedness directory, not a replacement for official tools.

What an AED is, and why location pages matter

An AED is an Automated External Defibrillator. It can deliver a shock to try to restart a heart during sudden cardiac arrest.

In Singapore, the Singapore Civil Defence Force (SCDF) publishes the Public Access AEDs dataset on data.gov.sg. The useful fields are AED_ID, operating hours, road name, floor level, latitude, longitude, and a free-text location description.

A pin on a map tells you a device exists nearby. It does not tell you that the nearest one is in a fire command centre, behind a school gate after hours, or on B2 next to pillar H in a car park.

Those details live in the dataset. They are just hard to use in the format they ship in.

A GeoJSON dump is not a product

I keep building tools from public datasets because a lot of government data is valuable and still awkward for regular people.

Researchers can work with a portal, an API, or a 6.9MB GeoJSON file. Someone asking "is there an AED in Junction 8?" usually cannot.

data.gov.sg also has a thinner three-column file with postal code, building name, and location description. I did not use it. It has no AED_ID, no hours, no floor, and no coordinates. That is a cross-check file, not a product database.

The rich GeoJSON still has the problems every directory hits on day one:

  • About 55% of rows have no building name
  • Some buildings have several AEDs that look like unrelated features
  • Location notes are written in different styles
  • Access is tied to operating hours, not just a pin
  • "Nearest" on a map can still be unreachable

The last ingest I shipped has 9,644 AEDs, 0 duplicate AED_IDs, and 0% missing coordinates. Roughly 6,572 of those rows look like 24/7 from the hours string. Around 1% of postal codes do not map to a URA planning area.

The human question behind the data is simple: where can I find an AED near this place, and can I actually get to it?

Raw features do not answer that. Pages do.

AEDLocator.sg is for Tuesday night, not the ambulance ride

This part matters more than the stack.

AEDLocator.sg is not meant to replace SCDF, 995, or myResponder. SCDF says the same thing on the dataset itself: for the most up-to-date public AED locations, use myResponder, and access depends on the operating hours of the premises.

I built a preparedness tool. The useful moment is when you look up:

  • Your HDB estate
  • Your parents' neighbourhood
  • The office
  • Your child's school
  • The gym
  • A mall you actually use
  • The MRT station you pass every day

If you only search when someone has collapsed, you are already late. Coordinates will not help you as much as "Level 1, outside the management office."

Why I did not ship a map-only MVP

The obvious demo is a map with pins. I almost stopped there, because that is what every open-data demo looks like.

AEDLocator does have a map. Search results load MapLibre only when that island mounts, so the rest of the page does not pay for a WebGL bundle.

A map is still a weak product on its own:

  1. Indoor wayfinding. "Level B1 Car Park Green Zone, Pillar H" is more useful than a lat/lng pair once you are inside the building.
  2. Shareable answers. People search for "AED at [building name]" and "AED near [postal code]." A single map URL is a weak answer for Google and a weak thing to text to a family WhatsApp group.
  3. Grouping. A mall, school, or MRT station can have several AEDs. Isolated pins hide the useful view: every device in this place, with floor and access notes.

So the information architecture is the product:

  • /aed/{AED_ID} for each device
  • /locations/building/{slug} when a building name exists
  • /locations/postal/{postal}
  • /locations/area/{ura-slug} from postal sector to URA planning area
  • /locations/mrt/{slug} and /locations/mall/{slug} derived from building names
  • MDX guides for questions a location page cannot answer

A user can land on a single device from search, then move to the rest of the building, the postal code, or a nearby MRT instead of hitting a dead end.

Next.js, JSON files, and a weekly ingest

The live site does not read a database. It reads normalized JSON.

That sounds unsophisticated until you look at the size. 9,644 records fit in memory. Search is a scored scan over aeds.json. Nearby results are haversine distance, not a spatial index. Vercel builds stay reproducible because the processed files ship with the repo.

The stack I actually run:

  • Next.js 16 (App Router) for pages, sitemaps, and ISR
  • Tailwind CSS 4 for a UI that puts reachability fields first
  • Vercel for hosting
  • MapLibre for the map island
  • MDX (gray-matter + next-mdx-remote) for guides

Pages revalidate every 24 hours. generateStaticParams prebuilds the first 1,500 AED URLs and building hubs with at least two devices. Everything else still resolves on demand.

I also generate segmented XML sitemaps for AEDs, buildings, postals, malls, and MRT stations. A directory with no sitemap is asking Google to discover thousands of pages by accident.

What I did not choose:

OptionWhy I skipped it
Map-only SPAFast to demo, weak for search, weak for indoor details
Thin 3-column source fileNo stable ID, hours, floor, or coordinates
Runtime database as the read path9.6k rows do not need one, and deploys get harder to reproduce
Inventing hours or walking timesTrust dies the first time a page disagrees with the source

Search scores building name, postal digits, area, road, and location description. "Near me" sorts by straight-line distance. Walking routes can wait. Guessing a walk time from a pin would be a lie with extra steps.

Design the page for someone who is already stressed

Most directories are built for browsing. This one has to work when someone is in a hurry, or when they are calmly checking a place they use every week.

I do not want a wall of prose. I want the critical fields easy to spot:

  • Building name, or road name when the building is missing
  • Exact location description
  • Floor level
  • Address and postal code
  • Operating hours, including the 24/7 flag when we can detect it
  • Other AEDs in the same building
  • Nearby related locations

This is the lesson I keep relearning with dataset products: the best field in the file is not always the most impressive field.

Latitude and longitude are great for maps and distance sorts. For a person standing in a mall, "Level 1, outside the management office" wins.

I did not build AEDLocator.sg only for SEO. SEO is still part of the product, because the queries are obvious:

  • AED near me Singapore
  • AED at [building name]
  • AED near [postal code]
  • AED in [mall name]
  • AED at [MRT station]

Thin pages will not hold those queries. A building page earns its URL when it includes every device in the building, floor information, access notes, and related locations. A postal page earns its URL when it helps someone discover AEDs close to that area. Search itself is noindex. It is a tool, not a landing page.

The goal is not to generate thousands of URLs. The goal is to generate thousands of useful answers.

That is also why I added guides. Finding a cabinet is only half the problem. People still ask what an AED does, whether a member of the public can use one, why access hours matter, and what to do before help arrives. The guides are MDX in the repo, not a second CMS. They are context, not a substitute for CPR or first-aid training.

What I would not do again

Using the thin source file because it looked simpler. Without AED_ID and hours, you cannot build a stable URL or a reachability page.

Trusting "nearest" as the only ranking. A closer pin behind a locked office is less useful than a slightly farther device in a 24-hour lobby.

Generating a building page for every row. More than half the dataset has no building name. Those records belong on postal and area pages.

Prebuilding every AED URL on first paint. 9,644 static pages is fine eventually. The first 1,500 plus dynamicParams shipped faster and still indexes.

Known limitations, on purpose

AEDLocator.sg is early, and some limits are load-bearing.

It is not real-time. Ingest is weekly, then a deploy of new JSON. myResponder remains the live source during an emergency.

Availability changes. A listed AED can be inside a school, office, or mall that is closed. Hours in the dataset can also be incomplete or oddly formatted. Seven rows currently have hours we cannot classify.

Building coverage is sparse by source, not by accident. About 55% of rows have no BUILDING_NAME. The UI falls back to road and postal. I would rather show a road name than invent a building.

Area mapping is approximate. Postal sector to URA planning area is good enough for browse pages. It is not a polygon point-in-polygon lookup.

Nearby is straight-line distance. That is honest. It is also not a walking route.

This is not medical advice. The site should make the data source obvious, show the last sync time, and send people to 995 when it counts.

What I want next is still in that same lane: better nearby discovery, clearer hours, tighter area mapping, and a better way to surface source diffs. The idea does not change. Take useful public data, clean it, and make it possible to act on.

If you want to see the result, open AEDLocator.sg and look up the postal code you actually use. Home, office, school, gym. That is the whole point.

If you are turning another public dataset into a directory and want to compare notes, hit me up on Twitter.