Open-source location intelligence toolkit. Parse freeform text, HTML, and URLs into geocoded locations; ship with types and React components for rendering maps.
npm install @dromney/mapthisYou'll also want one or more optional peer deps (see Subpath exports):
# pick what you use:
npm install openai js-tiktoken # @dromney/mapthis/ai (OpenAI)
npm install @anthropic-ai/sdk js-tiktoken # @dromney/mapthis/ai (Anthropic)
npm install @googlemaps/google-maps-services-js # @dromney/mapthis/geocoding
npm install html-to-text # @dromney/mapthis/scrape
npm install react react-dom @vis.gl/react-google-maps # @dromney/mapthis/reactimport { createLocationParser } from "@dromney/mapthis/ai"
import { createGeocoder } from "@dromney/mapthis/geocoding"
import { createMapGenerator } from "@dromney/mapthis/generate"
const generator = createMapGenerator({
parser: createLocationParser({ apiKey: process.env.OPENAI_API_KEY! }),
geocoder: createGeocoder({ provider: "google", apiKey: process.env.GOOGLE_MAPS_KEY! }),
})
const result = await generator.generateFromSource({
sourceType: "url",
source: "https://example.com/best-restaurants-tokyo",
})
console.log(result.title)
for (const p of result.places) {
if (p.data) console.log(p.data.name, p.data.lat, p.data.lng)
else console.warn("could not geocode", p.input.address, "—", p.error)
}sourceType accepts "url", "text", or "list". For "list", each newline-separated entry is geocoded directly with no LLM call (and no LLM cost).
@dromney/mapthis ships a root entry plus focused subpaths so consumers only load what they use.
| Subpath | What it contains | Optional peer deps |
|---|---|---|
@dromney/mapthis |
Re-exports types + utils (lightweight only) | — |
@dromney/mapthis/types |
Zod schemas + TS domain types (PlaceMap, Place, PlaceGroup, Partner, ParsedLocation, Coordinates, …) |
— |
@dromney/mapthis/utils |
Pure helpers: text, color, numbers, geo, stopwatch | — |
@dromney/mapthis/scrape |
getHtmlFromUrl, htmlToText, getTextFromUrl |
html-to-text |
@dromney/mapthis/search |
createSearchClient (Google Custom Search) |
— |
@dromney/mapthis/ai |
createLocationParser, createOpenAiBackend, createAnthropicBackend, summarizeText, prompts |
openai or @anthropic-ai/sdk, plus js-tiktoken |
@dromney/mapthis/geocoding |
createGeocoder (Google Maps) with pluggable GeocodingProvider |
@googlemaps/google-maps-services-js |
@dromney/mapthis/generate |
createMapGenerator — composes parser + geocoder into a pure orchestrator |
— (transitively whatever parser/geocoder use) |
@dromney/mapthis/react |
MapProvider, GoogleMapsViewer, PlaceMarker, autofit, browserAutocomplete |
react, react-dom, @vis.gl/react-google-maps |
createLocationParser accepts either an OpenAI config, an Anthropic config, or a bring-your-own backend implementing the LlmBackend interface.
// OpenAI (default)
createLocationParser({ apiKey: process.env.OPENAI_API_KEY! })
createLocationParser({ provider: "openai", apiKey: ..., model: "gpt-4o-mini" })
// Anthropic
createLocationParser({
provider: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
model: "claude-haiku-4-5-20251001",
})
// Bring-your-own (e.g. an Azure/Bedrock adapter or a deterministic test fake)
createLocationParser({ backend: myCustomBackend })Both bundled backends implement the same two-stage pipeline: a chunked summarization pass to fit the model's context window, followed by a structured-output call (OpenAI function calling, Anthropic tool use) to extract the location list.
createMapGenerator is intentionally pure — it returns a plain object and never touches a database. Wire the result into your own persistence layer:
const result = await generator.generateFromSource({ sourceType: "url", source: url })
await db.$transaction(async (tx) => {
const map = await tx.map.create({ data: { title: result.title /* … */ } })
const group = await tx.placeGroup.create({ data: { mapId: map.id /* … */ } })
await tx.place.createMany({
data: result.places.map((p, i) => ({
mapId: map.id,
groupId: group.id,
position: i,
query: p.input.address,
description: p.input.description,
name: p.data?.name,
address: p.data?.address,
lat: p.data?.lat,
lon: p.data?.lng,
provider: p.data?.provider,
providerId: p.data?.providerId,
error: p.error,
locationPromptVersion: result.locationPromptVersion,
summaryPromptVersion: result.summaryPromptVersion,
})),
})
})Every error thrown by the package extends MapthisError (@dromney/mapthis/types). Domain-specific subclasses let callers handle the failure modes that matter to their UX:
| Class | Subpath | When |
|---|---|---|
InvalidUrlError, ScrapeError |
/scrape |
URL was unreachable or returned non-HTML |
NoSearchResultsError, SearchError |
/search |
Google CSE returned zero usable results |
NoLocationsFoundError, AiInputLengthError, AiOutputLengthError, AiResponseJsonError, InvalidJsonSchemaError, SummarizeTextError, AiError |
/ai |
LLM stage failed |
GeocodingError |
/geocoding |
Geocoder threw (per-input failures resolve, not throw) |
Per-input geocoder failures are not thrown — geocodeMany returns a PlaceQueryResult with data: null and error: string so one bad address can't fail an entire batch.
- No
process.envreads. All secrets are passed to factory functions by the consumer. Keeps the package portable and testable. - Framework-agnostic. Core modules are pure TypeScript. React components live behind the
@dromney/mapthis/reactsubpath so server-only consumers skip them entirely. - Plain domain types. Hand-rolled TS with matching Zod schemas. Consumers aren't locked to a particular ORM — write adapter functions between your DB models and these types in your app.
- Optional peer deps for heavy things. Every SDK is an optional peer dependency. Install only what the subpaths you use need.
Apache 2.0 — see LICENSE.