@dromney/mapthis
v0.2.0
Published
Open-source location intelligence: parse freeform text, HTML, and URLs into geocoded locations, with types and React components.
Maintainers
Readme
@dromney/mapthis
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/reactQuick start
import { 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).
Subpath exports
@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 |
Choosing an LLM backend
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.
Persisting results
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,
})),
})
})Error handling
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.
Design principles
- 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.
License
Apache 2.0 — see LICENSE.
