Routing around Google Maps in Korea: Naver & Kakao deep links, weird coordinates, and iOS clipboard
If you've traveled to Korea, you've hit this wall: Google Maps can't give you walking or transit directions here. Map-data export is restricted, so locals use Naver Map or KakaoMap instead. The usual workaround for visitors is painful — copy a place's Korean name from Google, paste it into Naver, repeat for every stop.
So I built K-Map Router: paste a Google Maps link → it opens that place (and the route) in Naver or Kakao. Free, no sign-up, nothing stored. Code is here: github.com/piyaklabs/k-map-router.
This post isn't a "look how hard I worked" story — honestly, I shipped it fast with heavy AI pair-programming (Claude Code). It's a field guide to the stuff that's genuinely hard to find documented : how Korean map deep links and coordinates actually work. If you ever build something in this space, I hope this saves you a few days.
The architecture, in one breath
A single Cloudflare Worker serves both the React SPA (static assets) and the POST /api/resolve endpoint — same origin,
free tier, zero runtime dependencies. Coordinate resolution is server-side (the browser → Google is blocked by CORS) and
is nothing but fetch + regex + a little decoding. No DB, stateless.
Hard part #1: coordinates live in different formats per URL
You can't just regex one pattern. A Google Maps URL (after following redirects) hides the coordinates in one of several shapes, and you have to try them in priority order:
// 1) place pin — most authoritative
// ...!3d{lat}!4d{lng}
// 2) directions waypoint — ⚠️ REVERSED: !1d{lng}!2d{lat}
// multiple pairs => last = destination, first = origin
// 3) viewport center — /@{lat},{lng},17z
// 4) ?query= / &destination= / &daddr=
// 5) ?ll= / &sll=
The one that bit me hardest: /dir/ directions URLs store !1d{longitude}!2d{latitude} — longitude first. Read it as
(lat, lng) and you'll happily return a point that's in the ocean. And when there are multiple pairs, the last pair is the
destination, the first is the start point (which is how the tool can preserve A→B routes).
Hard part #2: mobile "Copy link" hides coordinates in a protobuf
Links shared from the Google Maps mobile app (the ones with ?g_st=...) are special. They resolve to something like:
.../maps?saddr=Seoul+Station&daddr=Gyeongbokgung&geocode=FWoPPQId...;FWFrPQIdEYSRBy...
There are no plaintext coordinates anywhere — they're base64url-encoded in the geocode= param, as a tiny protobuf. Each
;-separated entry encodes one endpoint:
// 0x15 = field 2, fixed32 (little-endian) = lat * 1e6
// 0x1D = field 3, fixed32 (little-endian) = lng * 1e6
function decodeGeocodeEntry(entry) {
const bin = atob(entry.replace(/-/g, "+").replace(/_/g, "/"));
let lat = null, lng = null;
for (let i = 0; i + 4 < bin.length && (lat === null || lng === null); i++) {
const tag = bin.charCodeAt(i);
if (tag !== 0x15 && tag !== 0x1d) continue;
let v = 0;
for (let j = 3; j >= 0; j--) v = v * 256 + bin.charCodeAt(i + 1 + j); // LE
if (v > 0x7fffffff) v -= 0x100000000;
if (tag === 0x15) lat = v / 1e6; else lng = v / 1e6;
i += 4;
}
return lat !== null && lng !== null ? { lat, lng } : null;
}
Verified against 경복궁 (Gyeongbokgung): FWFrPQIdEYSRBy... → 37.579617, 126.977041. ✅
Hard part #3: the deep link specs (and their gotchas)
Naver (primary — best transit + English):
nmap://route/public?dlat={lat}&dlng={lng}&dname={enc}&appname={APPNAME}
appnameis required (silently fails without it).dnameis optional — omit it and Naver shows the real address. Don't send a literalDestinationplaceholder.- Modes are different action paths:
route/walk,route/car,route/public.
Kakao (secondary):
kakaomap://route?ep={lat},{lng}&by=publictransit
- Modes:
by=foot | car | publictransit.
Android: custom schemes are flaky from Chrome. Use an intent:// URL with a built-in store fallback:
intent://route/public?...#Intent;scheme=nmap;package=com.nhn.android.nmap;S.browser_fallback_url=...;end
Hard part #4: Kakao's web URL uses a coordinate system from another dimension
For desktop fallback, Kakao's legacy link/to API can't take a start point. But its redirect target can — if you feed it
Kakao's internal WCongnamul coordinates:
https://map.kakao.com/?map_type=TYPE_MAP&target=traffic&rt={sx},{sy},{ex},{ey}&rt1={from}&rt2={to}
WCongnamul turned out to be EPSG:5181 (a GRS80 Transverse Mercator) scaled ×2.5. I implemented the projection by hand and
it matched Kakao's own conversion to the integer for every test point. (Also: never put a comma in the rt1/rt2 label — it
breaks the parser and silently drops the destination.)
Hard part #5: the iOS clipboard "paste" button that wouldn't paste
The "Paste from clipboard" button worked everywhere except iOS. Two reasons, both subtle:
- Google Maps "Copy link" puts the URL on the clipboard as
text/uri-listonly — notext/plain. Sonavigator.clipboard.readText()returns an empty string. - iOS WebKit expires the user activation after your first
await. So areadText()→ fall back toread()chain always fails on the second call withNotAllowedError.
The fix is to make exactly one clipboard call inside the gesture, then read the type off the already-resolved
ClipboardItem (those getType calls reuse the granted permission):
const items = await navigator.clipboard.read(); // one call, in the gesture
for (const item of items) {
for (const type of ["text/uri-list", "text/plain", "text/html"]) {
if (!item.types.includes(type)) continue;
const text = (await (await item.getType(type)).text()).trim();
// uri-list: first non-comment line is the URL
if (text) return text;
}
}
The iOS "Paste" permission bubble itself is unavoidable — it's OS-enforced for any programmatic clipboard read.
Bonus: respect the mode the user already picked
Google encodes the travel mode in the link (travelmode=driving, dirflg=d, or !3e0). Reading it means a shared driving
route opens directly in driving directions in Naver/Kakao — "plan in Google Maps, navigate in Korea," unchanged.
Try it / take it
// Detect dark theme var iframe = document.getElementById('tweet-2067124302403772609-944'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2067124302403772609&theme=dark" }
- Live: kmap.piyaklabs.com
- Code (MIT-ish, stateless, zero deps): github.com/piyaklabs/k-map-router
If you're building anything that bridges Google Maps and Korean map apps, steal the deep-link and coordinate logic — that's exactly why it's public. Questions welcome. 🐣
Discussion in the ATmosphere