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.
hourscomes 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[]>;
};| Method | Description | Source |
|---|---|---|
geocode | Resolve a typed area to one point. null when nothing matches. | OpenStreetMap Nominatim |
search | Up 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
| Source | What it provides | Access |
|---|---|---|
| OpenStreetMap Nominatim | Place-name geocoding | Public, no key. Bound by the usage policy — one request per second, identifying User-Agent required |
| OpenStreetMap Overpass | Amenity search by radius | Public, 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.