Skip to Content
ToolsNearby care

Nearby care

The nearby care tool (ctx.NEARBY_CARE) is a built-in tool available inside any loop. It answers “where’s the closest pharmacy?” in two steps: geocode turns a typed area into coordinates, and search finds places of one kind around a point, nearest first.

The two steps are separate so a loop that already has coordinates — from a shared location pin, or a member record — can skip the geocoding entirely.

What this tool is for

  • Finding the nearest pharmacy, clinic, hospital, or park to a point.
  • Turning a typed area (“Sandton”, “Khayelitsha, Cape Town”) into coordinates.
  • Giving a member a distance and a maps link they can tap.

What this tool is not for

  • Not a stock or availability check. That a pharmacy exists says nothing about whether it has a medicine.
  • Not authoritative opening hours. hours comes from the map and is often stale or absent. If you quote it, say where it came from.
  • Not complete coverage. OpenStreetMap is crowd-sourced. Coverage is good in cities and thin in rural areas — an empty result frequently means “not mapped”, not “none nearby”.
  • Not a location guess. Never invent coordinates. With no pin and no typed area, ask for one.

Tool API

Must be called inside a step.do callback — ctx only exists there. See Step context.

type NearbyCareKind = "pharmacy" | "clinic" | "hospital" | "park"; type GeocodedPlace = { latitude: number; longitude: number; label: string; }; type NearbyPlace = { name: string; kind: NearbyCareKind; /** Human-readable, e.g. "683 m" or "1.1 km". */ distance: string; meters: number; hours: string | null; mapsUrl: string; }; type NearbyCareTool = { geocode(input: { place: string; /** ISO 3166-1 alpha-2 bias. Defaults to "za"; pass null for no bias. */ countryCode?: string | null; }): Promise<GeocodedPlace | null>; search(input: { latitude: number; longitude: number; kind: NearbyCareKind; /** Defaults to 3000. */ radiusMeters?: number; }): Promise<NearbyPlace[]>; };
MethodDescriptionSource
geocodeResolve a typed area to one point. null when nothing matches.OpenStreetMap Nominatim
searchUp to five places of one kind within the radius, nearest first. Empty when nothing’s mapped.OpenStreetMap Overpass

geocode biases to South Africa by default — pass a different countryCode, or null, to search elsewhere. search returns places sorted by true great-circle distance from the origin, with both a formatted distance and raw meters so you can sort or threshold yourself.

Both methods throw on invalid input (a one-character place, an out-of-range latitude, an unknown kind) and return null / [] on a genuine miss or an upstream failure.

Overpass reliability

The public Overpass instance sheds load with a 504 under contention. search retries up to three times with a short backoff before giving up and returning an empty array, which covers the common transient case. A loop should still treat an empty result as “couldn’t find anything right now” and offer a wider radius or a different kind rather than asserting there’s nothing nearby.

Example: find a pharmacy from a pin or a typed area

import { Workflow } from "./loop"; export class NearbyLoop extends Workflow { async run( event: HealthEvent<{ latitude?: number; longitude?: number; area?: string; kind?: "pharmacy" | "clinic" | "hospital" | "park"; }>, step: HealthStep, ) { const kind = event.body.kind ?? "pharmacy"; const origin = await step.do("Resolve a location", async ({ ctx }) => { if (event.body.latitude != null && event.body.longitude != null) { return { latitude: event.body.latitude, longitude: event.body.longitude, label: null as string | null, }; } return event.body.area ? ctx.NEARBY_CARE.geocode({ place: event.body.area }) : null; }); if (!origin) { // Never guess a location — ask for one. return { action: "ask_for_location", kind }; } const places = await step.do("Search nearby", async ({ ctx }) => ctx.NEARBY_CARE.search({ latitude: origin.latitude, longitude: origin.longitude, kind, }), ); if (places.length === 0) { return { action: "widen_search", kind, searchedFrom: origin.label }; } return { action: "respond", searchedFrom: origin.label, // Name the closest two or three with distance; hours may be stale. closest: places.slice(0, 3), }; } }

PHI posture

Geocoding sends a place name to Nominatim, and search sends coordinates to Overpass. A member’s location is sensitive: a coordinate pair is often enough to identify a home. Nothing is stored by the tool, but the coordinates land in your loop’s step.do row like any other step input — decide deliberately whether the run needs to keep them.

Sources and usage policy

SourceWhat it providesAccess
OpenStreetMap NominatimPlace-name geocodingPublic, no key. Bound by the usage policy  — one request per second, identifying User-Agent required
OpenStreetMap OverpassAmenity search by radiusPublic, no key. Shared instance; expect intermittent 504s under load

Both are called with an identifying User-Agent, as the OSM policies require. High-volume loops should cache geocoding results rather than resolving the same area on every run.

Last updated on