Documentation
HTTP API
The demo exposes public read-only HTTP endpoints at https://geonames-global-demo.vimaksh.workers.dev. Each accepts GET only. These routes belong to geonames-global-demo; geonames-global-service is an internal RPC provider, not a public HTTP API. Use the Countries page to fetch country and continent names from the browser; select a tab and click Fetch to load its names.
The web Worker calls the provider through the GEONAMES Service Binding. Caller and provider must belong to the same Cloudflare account. The service owns D1 and KV; the caller needs neither database ID nor direct D1/KV bindings.
Unknown paths return plain-text 404 Not found. A non-GET request to a known route returns 405 with {"error":"Method not allowed"} and Allow: GET. Invalid HTTP parameters return 400 with a JSON error string. Failed search, countries, or continents RPC calls return 502 with a JSON error string. The get and get-many routes do not translate service failures into a route-level 502; the provider’s fetch() still returns 404.
Search defaults and geographic filters are described in Worldwide search.
GET /api/search
Section titled “GET /api/search”| Parameter | Required | Contract |
|---|---|---|
q |
No | Search text. Missing or whitespace-only text returns an empty page. |
limit |
No | Defaults to 25. Integer values clamp to 2–50. A non-integer value uses 25. |
offset |
No | Defaults to 0. Must be a nonnegative safe integer or returns 400. |
countryCode |
No | Two-letter GeoNames country code filter, e.g. IN. |
admin1Code |
No | Raw GeoNames admin1 component, e.g. 19 for Karnataka. Do not pass qualified IN.19; qualification is only used for reference joins. Requires country context from countryCode or parentId. |
admin2Code |
No | Raw GeoNames admin2 component, e.g. 572. Do not pass qualified IN.19.572; qualification is only used for reference joins. Requires country and admin1 context, supplied explicitly or by the parent. |
parentId |
No | Positive GeoNames ID of a country (PCLI), state/region (ADM1) or district (ADM2). Strict descendants only. Compatible code filters can narrow the scope; conflicting filters return 400. |
useDefaultScope |
No | Literal true or false. false skips configured country-first behavior but never removes explicit filters. |
parentId composes with supplied country/admin filters as an intersection: results must be strict descendants and match every supplied filter. The selected parent row itself is excluded. Conflicting hierarchy filters return 400; the parent and explicit filters are not mutually exclusive. |
Example: /api/search?q=Bengaluru&countryCode=IN&admin1Code=19
The response is { "results": Location[], "nextOffset": number | null, "durationMs": number, "timings": { "rpcMs": number, "rpc": TimingSpan | null, "service": ServiceTimings | null } }. nextOffset is the offset for the next page or null. Timings are runtime measurements and vary.
Blank q returns empty results, nextOffset: null, durationMs: 0, and zero/null RPC timings without calling the service. Successful nonblank searches may carry X-GeoNames-Cache: HIT|MISS|BYPASS.
Response examples
Section titled “Response examples”The search results below were captured from the local API. Aliases are shortened to the first three entries. Search and error excerpts omit durationMs and timings for readability; the complete HTTP envelope is described above, and the timing types are listed below. Timing values vary by request. Get/get-many return the full location shape without search timing metadata.
Search request:
GET /api/search?q=Bengaluru&limit=2&offset=0&countryCode=IN&admin1Code=19{ "results": [ { "id": 1277333, "name": "Bengaluru", "asciiname": "Bengaluru", "countryCode": "IN", "countryName": "India", "admin1Code": "19", "admin1Name": "Karnataka", "admin2Code": "572", "admin2Name": "Bangalore Urban", "featureClass": "P", "featureCode": "PPLA", "featureName": "seat of a first-order administrative division", "featureDescription": "seat of a first-order administrative division (PPLC takes precedence over PPLA)", "latitude": 12.97194, "longitude": 77.59369, "population": 8495492, "timezone": "Asia/Kolkata", "aliases": ["BLR", "Ban'nkalor", "Bangalor"] }, { "id": 1277331, "name": "Bangalore Urban", "asciiname": "Bangalore Urban", "countryCode": "IN", "countryName": "India", "admin1Code": "19", "admin1Name": "Karnataka", "admin2Code": "572", "admin2Name": "Bangalore Urban", "featureClass": "A", "featureCode": "ADM2", "featureName": "second-order administrative division", "featureDescription": "a subdivision of a first-order administrative division", "latitude": 13, "longitude": 77.58333, "population": 9621551, "timezone": "Asia/Kolkata", "aliases": ["BUR", "Bangalore", "Bengaluru Urban"] } ], "nextOffset": 2}Follow the returned nextOffset exactly, keeping the same query, limit, and every filter:
GET /api/search?q=Bengaluru&limit=2&offset=2&countryCode=IN&admin1Code=19The response has the same complete metadata shape; nextOffset is either the next offset or null when exhausted.
No-match excerpt, captured with an explicit India scope so the request cannot fall back internationally:
GET /api/search?q=Reykjavik&limit=2&countryCode=IN{"results":[],"nextOffset":null}Blank-query example (no service binding call, so service timings are null):
{"results":[],"nextOffset":null,"durationMs":0,"timings":{"rpcMs":0,"rpc":null,"service":null}}Invalid-offset request:
GET /api/search?q=Bengaluru&offset=-1Response (HTTP 400):
{"error":"Use a nonnegative integer offset."}Validation error text can depend on the invalid parameter.
Get and get-many response examples
Section titled “Get and get-many response examples”GET /api/get?id=1277333 returns a full location or null:
{"location":{"id":1277333,"name":"Bengaluru","asciiname":"Bengaluru","countryCode":"IN","countryName":"India","admin1Code":"19","admin1Name":"Karnataka","admin2Code":"572","admin2Name":"Bangalore Urban","featureClass":"P","featureCode":"PPLA","featureName":"seat of a first-order administrative division","featureDescription":"seat of a first-order administrative division (PPLC takes precedence over PPLA)","latitude":12.97194,"longitude":77.59369,"population":8495492,"timezone":"Asia/Kolkata","aliases":["BLR","Ban'nkalor","Bangalor"]}}GET /api/get?id=999999999:
{"location":null}GET /api/get-many?ids=1277333,999999999 returns known rows only, preserving input order (illustrative abbreviated aliases):
{"locations":[{"id":1277333,"name":"Bengaluru","asciiname":"Bengaluru","countryCode":"IN","countryName":"India","admin1Code":"19","admin1Name":"Karnataka","admin2Code":"572","admin2Name":"Bangalore Urban","featureClass":"P","featureCode":"PPLA","featureName":"seat of a first-order administrative division","featureDescription":"seat of a first-order administrative division (PPLC takes precedence over PPLA)","latitude":12.97194,"longitude":77.59369,"population":8495492,"timezone":"Asia/Kolkata","aliases":["BLR","Ban'nkalor","Bangalor"]}]}GET /api/get
Section titled “GET /api/get”Get one location by GeoNames ID. id is required and must be a positive safe integer; invalid values return 400. Example: /api/get?id=1277333. Response: { "location": Location | null }; an unknown ID returns location: null.
GET /api/get-many
Section titled “GET /api/get-many”Get locations by comma-separated GeoNames IDs. ids is required and must contain 1–100 positive safe integers; malformed or oversized lists return 400. Example: /api/get-many?ids=1277333,1262321,999999999. Response: { "locations": Location[] }. Unknown IDs are omitted; input order and repeated known IDs are preserved.
The demo public routes require no authentication. Protected administrative endpoints are described in Admin settings and operational status.
Location shape
Section titled “Location shape”All lookup methods return the same complete location record:
interface Location { id: number; name: string; asciiname: string; countryCode: string; countryName: string | null; admin1Code: string; admin1Name: string | null; admin2Code: string; admin2Name: string | null; featureClass: string; featureCode: string; featureName: string | null; featureDescription: string | null; latitude: number; longitude: number; population: number; timezone: string; aliases: string[];}Names resolved through reference tables are nullable when a matching reference row is absent. aliases is an array (malformed stored JSON maps to an empty array). Coordinates are decimal degrees, converted from stored millionths.
Service Binding RPC
Section titled “Service Binding RPC”The HTTP routes above belong to geonames-global-demo. Bind directly only if a Worker in the same Cloudflare account needs typed RPC. Use provider service name geonames-global-service, caller binding name GEONAMES; no database ID or direct D1/KV binding is needed. The provider’s fetch() intentionally returns 404; RPC methods do not call it. Deploy the provider before its consumers.
Minimal caller wrangler.jsonc:
{ "name": "my-geonames-consumer", "main": "src/index.ts", "compatibility_date": "2026-10-01", "services": [ { "binding": "GEONAMES", "service": "geonames-global-service" } ]}Country and continent lists
Section titled “Country and continent lists”The demo exposes GET /api/countries and GET /api/continents. Both call the corresponding GEONAMES RPC method and return a plain JSON array without an envelope or pagination. Country entries have shape { code: string, name: string, continent: string }; continent entries have shape { code: string, name: string }. Non-GET requests return 405 with Allow: GET; RPC failures return 502 with a JSON error string. The internal provider’s HTTP fetch() remains 404—these are web Worker HTTP routes, not provider REST endpoints.
The Countries page has Countries and Continents tabs. Names are requested only after selecting a tab and clicking Fetch.
The underlying methods remain read-only RPC:
const countries = await env.GEONAMES.countries();// Country[]: { code: "IN", name: "India", continent: "AS" }
const continents = await env.GEONAMES.continents();// Continent[]: { code: "AS", name: "Asia" }countries()returns every imported country/territory reference entry, ordered by name and then code. The list is worldwide, independent of search settings and imported location rows. An empty reference table returns[]; database failures reject the RPC.continents()returns the seven GeoNames continents, ordered by name:AFAfrica,ANAntarctica,ASAsia,EUEurope,NANorth America,OCOceania, andSASouth America. This fixed catalog does not query D1 and remains available before country data is imported.
Country continent values correspond to continent code values. Both methods take no arguments. The country list uses the existing countryInfo.txt import; no new migration or import is needed for an already populated database. Codes follow the GeoNames country catalog.
Typed caller example
Section titled “Typed caller example”Copy the public type and caller shape below. SearchOptions supports country/admin filters, parent scoping, and opting out of the configured default. Direct RPC validation differs from HTTP validation; see scope and paging.
import { WorkerEntrypoint } from "cloudflare:workers";
interface Location { id: number; name: string; asciiname: string; countryCode: string; countryName: string | null; admin1Code: string; admin1Name: string | null; admin2Code: string; admin2Name: string | null; featureClass: string; featureCode: string; featureName: string | null; featureDescription: string | null; latitude: number; longitude: number; population: number; timezone: string; aliases: string[];}interface Country { code: string; name: string; continent: string }interface Continent { code: string; name: string }interface SearchOptions { countryCode?: string; admin1Code?: string; admin2Code?: string; parentId?: number; useDefaultScope?: boolean;}interface SearchSettings { defaultCountryCode: string; defaultScopeEnabled: boolean; fallbackToGlobal: boolean;}interface TimingSpan { startMs: number; endMs: number }interface D1QueryTiming extends TimingSpan { name: string; label: string; reason: string; retrieves: string; outcome: string | null; durationMs: number;}type StageDecision = | { status: "not-run" } | { status: "skipped"; reason: string } | { status: "ran"; reason: string };type FuzzyExecution = | { status: "not-run" } | { status: "skipped"; reason: string } | { status: "ran"; reason: string; candidateLimit: number; candidateCount: number; rankedCount: number; selectedCount: number; };interface SearchExecution { normalizedQuery: string | null; path: "not-run" | "empty" | "short-prefix" | "fts"; ftsExpression: string | null; trace: Array< | { kind: "decision"; stage: string; reason: string; outcome: string } | { kind: "d1"; operationIndex: number } >; fuzzy: FuzzyExecution; secondFts: StageDecision; resultCount: number | null;}interface ServiceTimings { serviceMs: number; kvReadMs: number | null; kvWriteMs: number | null; normalizationMs: number; firstFtsMs: number | null; secondFtsMs: number | null; ftsMs: number | null; fuzzyMs: number | null; fuzzyCandidateD1Ms: number; getManyD1Ms: number; queryEngineMs: number | null; d1Queries: number; d1Ms: number; d1: D1QueryTiming[]; execution: SearchExecution; spans: { service: TimingSpan | null; kvRead: TimingSpan | null; kvWrite: TimingSpan | null; normalization: TimingSpan[]; firstFts: TimingSpan | null; secondFts: TimingSpan | null; fuzzy: TimingSpan | null; queryEngine: TimingSpan | null; };}type SearchPage = { results: Location[]; nextOffset: number | null; cacheStatus: "MISS" | "HIT" | "BYPASS"; timings: ServiceTimings;};interface GeoNamesService extends WorkerEntrypoint { countries(): Promise<Country[]>; continents(): Continent[]; get(id: number): Promise<Location | null>; getMany(ids: number[]): Promise<Location[]>; search(query: string, limit?: number, options?: SearchOptions): Promise<Location[]>; searchPage(query: string, limit?: number, offset?: number, options?: SearchOptions): Promise<SearchPage>; searchSettings(): Promise<SearchSettings>; updateSearchSettings(settings: SearchSettings): Promise<SearchSettings>;}interface Env { GEONAMES: Service<GeoNamesService> }
export default { async fetch(_request: Request, env: Env): Promise<Response> { const options: SearchOptions = { countryCode: "IN", admin1Code: "19", useDefaultScope: false, }; const countries = await env.GEONAMES.countries(); const continents = await env.GEONAMES.continents(); const one = await env.GEONAMES.get(1277333); const many = await env.GEONAMES.getMany([1277333, 1262321]); const matches = await env.GEONAMES.search("Bengaluru", 10, options); const page = await env.GEONAMES.searchPage("Bengaluru", 25, 0, options); return Response.json({ countries, continents, one, many, matches, page }); },};get returns null for invalid or unknown IDs. getMany filters invalid IDs, preserves found input order and duplicates, and batches unique IDs internally. search returns Location[]; searchPage returns { results, nextOffset, cacheStatus, timings }. Use returned nextOffset until it is null, preserving query, limit, and every scope option.
Administrative RPC is a separate authenticated surface. adminSnapshot() returns AdminServiceSnapshot; the web Worker authenticates GET /api/admin before calling it. searchSettings() and updateSearchSettings(settings) manage protected default-scope configuration; see Admin settings.