@midnight_poet/freemaps
v0.1.0
Published
Free Google Maps iframe embeds with routing, pinning and distance calculations powered by OSM/OSRM — no API key, no billing account.
Maintainers
Readme
@projecthanif/freemaps
Google Maps iframe embeds with routing, pinning, and distance calculations — with no API key, no billing account, and no cost.
The trick: Google's keyless output=embed URLs handle everything visual (maps, pins, turn-by-turn directions), while free open services handle the math:
| Feature | Powered by | Free? |
|---|---|---|
| Map display, pins, place search | Google keyless embed | ✅ |
| Turn-by-turn directions (visual) | Google /maps/dir/ embed | ✅ |
| Geocoding (address → coords) | Nominatim (OpenStreetMap) | ✅ |
| Road distance + travel time | OSRM | ✅ |
| Straight-line distance | Haversine (pure JS, no network) | ✅ |
| Distance matrix (N×N points) | OSRM /table | ✅ |
Works in Node ≥18 and the browser. ESM + CJS, fully typed, zero runtime dependencies.
Install
npm i @projecthanif/freemapsQuick start
import { FreeMaps } from "@projecthanif/freemaps";
const maps = new FreeMaps({
embed: { region: "ng" },
geocoder: {
// Required by Nominatim's usage policy — identify your app:
userAgent: "myapp.com [email protected]", // used server-side (Node)
email: "[email protected]", // used client-side (browsers)
},
});
// --- Embed URLs (drop into any <iframe src="...">) ---
maps.placeEmbed("Eiffel Tower");
maps.pinEmbed([9.0563, 7.4985], "Our Office", 17);
maps.directionsEmbed("Ikeja, Lagos", "Lekki Phase 1", "driving");
// Browser: create the element directly
document.body.append(
maps.embed.iframeElement(maps.placeEmbed("Wuse 2, Abuja"), { height: "450" }),
);
// --- Geocoding (results cached automatically) ---
await maps.geocode("Maitama, Abuja"); // { lat, lng, label } | null
await maps.reverseGeocode([9.0785, 7.4951]); // "Maitama, Abuja, ..." | null
// --- Distances: accepts coordinates OR raw address strings ---
await maps.distanceKm("Abuja", "Lagos"); // straight line
await maps.roadDistanceKm([9.07, 7.39], [6.52, 3.37]); // real roads
// --- Full route object ---
const route = await maps.route("Wuse 2, Abuja", "Gwarinpa, Abuja");
route.distanceKm; // 14.2
route.humanDuration; // "27 min"
route.geometry; // [[lat, lng], ...] — feed straight into Leaflet
// --- N×N matrix (delivery routing, nearest-branch, etc.) ---
const m = await maps.distanceMatrix([warehouse, stopA, stopB]);
m.distances[0][2]; // meters from warehouse to stopBReact example
function DirectionsMap({ from, to }: { from: string; to: string }) {
const maps = useMemo(() => new FreeMaps(), []);
const [stats, setStats] = useState<RouteResult | null>(null);
useEffect(() => {
maps.route(from, to).then(setStats).catch(() => setStats(null));
}, [from, to]);
return (
<div>
{stats && <p>{stats.humanDistance} · ~{stats.humanDuration}</p>}
<iframe
src={maps.directionsEmbed(from, to)}
width="100%"
height="450"
style={{ border: 0 }}
loading="lazy"
allowFullScreen
/>
</div>
);
}Individual modules
Everything is tree-shakeable — import only what you need:
import { EmbedBuilder, Geocoder, Router, haversineKm } from "@projecthanif/freemaps";Custom cache
Geocode results are cached in-memory by default. Swap in anything with get/set (localStorage, Redis, your DB):
const maps = new FreeMaps({
cache: {
get: async (k) => JSON.parse(localStorage.getItem(k) ?? "null") ?? undefined,
set: async (k, v) => localStorage.setItem(k, JSON.stringify(v)),
},
});Honest limitations
- The iframe is display-only. Your page's JS cannot read from or draw inside Google's embed, and the keyless embed supports only one pin per map. For multiple custom pins or drawn routes, render
route.geometrywith Leaflet (also free). - Public server etiquette. Nominatim allows ~1 req/sec (the built-in cache keeps you under it for repeat lookups); the OSRM demo server is for development/light use. For production, self-host OSRM (one docker command per region — extracts at Geofabrik) and set
router: { baseUrl: "http://localhost:5000" }. - CORS note: Nominatim and the OSRM demo both send CORS headers, so direct browser calls work — but for production traffic you should proxy through your backend anyway to add caching and rate limiting.
- Google doesn't document the keyless embed formally; it has been stable for many years but treat it as best-effort.
License
MIT
