Routing around Google Maps in Korea: Naver & Kakao deep links, weird coordinates, and iOS clipboard A developer built K-Map Router, a tool that converts Google Maps links into Naver Map or KakaoMap deep links for use in South Korea, where Google Maps lacks walking and transit directions due to map-data export restrictions. The project, deployed on a Cloudflare Worker with zero runtime dependencies, required reverse-engineering undocumented coordinate formats, including protobuf-encoded geocodes from the Google Maps mobile app and the EPSG:5181 coordinate system used by KakaoMap. The developer documented the deep link formats and coordinate decoding logic to help others navigate the fragmented Korean mapping ecosystem. 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 https://kmap.piyaklabs.com : paste a Google Maps link → it opens that place and the route in 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. 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. 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 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 . ✅ Naver primary — best transit + English : nmap://route/public?dlat={lat}&dlng={lng}&dname={enc}&appname={APPNAME} appname is dname is optional — omit it and Naver shows the real address. Don't send a literal Destination placeholder. route/walk , route/car , route/public . Kakao secondary : kakaomap://route?ep={lat},{lng}&by=publictransit 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 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. The "Paste from clipboard" button worked everywhere except iOS. Two reasons, both subtle: text/uri-list only text/plain . So navigator.clipboard.readText returns an empty string. await . So a readText → fall back to read chain NotAllowedError .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 : js 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. 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. // 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" } 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. 🐣