{"slug": "routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and", "title": "Routing around Google Maps in Korea: Naver & Kakao deep links, weird coordinates, and iOS clipboard", "summary": "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.", "body_md": "If you've traveled to Korea, you've hit this wall: **Google Maps can't give you walking or transit directions here.**\n\nMap-data export is restricted, so locals use **Naver Map** or **KakaoMap** instead. The usual workaround for visitors is\n\npainful — copy a place's Korean name from Google, paste it into Naver, repeat for every stop.\n\nSo I built [ K-Map Router](https://kmap.piyaklabs.com): paste a Google Maps link → it opens that place (and the route) in\n\nThis post isn't a \"look how hard I worked\" story — honestly, I shipped it fast with heavy AI pair-programming (Claude Code).\n\nIt's a **field guide to the stuff that's genuinely hard to find documented**: how Korean map deep links and coordinates\n\nactually work. If you ever build something in this space, I hope this saves you a few days.\n\nA single **Cloudflare Worker** serves both the React SPA (static assets) and the `POST /api/resolve`\n\nendpoint — same origin,\n\nfree tier, **zero runtime dependencies**. Coordinate resolution is server-side (the browser → Google is blocked by CORS) and\n\nis nothing but `fetch`\n\n+ regex + a little decoding. No DB, stateless.\n\nYou can't just regex one pattern. A Google Maps URL (after following redirects) hides the coordinates in one of several\n\nshapes, and you have to try them in priority order:\n\n```\n  // 1) place pin — most authoritative\n  //    ...!3d{lat}!4d{lng}\n  // 2) directions waypoint — ⚠️ REVERSED: !1d{lng}!2d{lat}\n  //    multiple pairs => last = destination, first = origin\n  // 3) viewport center — /@{lat},{lng},17z\n  // 4) ?query= / &destination= / &daddr=\n  // 5) ?ll= / &sll=\n```\n\nThe one that bit me hardest: ** /dir/ directions URLs store !1d{longitude}!2d{latitude} — longitude first.** Read it as\n\n`(lat, lng)`\n\nand 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=...`\n\n) are special. They resolve to something like:\n\n```\n  .../maps?saddr=Seoul+Station&daddr=Gyeongbokgung&geocode=FWoPPQId...;FWFrPQIdEYSRBy...\n```\n\nThere are **no plaintext coordinates anywhere** — they're base64url-encoded in the `geocode=`\n\nparam, as a tiny protobuf. Each\n\n`;`\n\n-separated entry encodes one endpoint:\n\n```\n  // 0x15 = field 2, fixed32 (little-endian) = lat * 1e6\n  // 0x1D = field 3, fixed32 (little-endian) = lng * 1e6\n  function decodeGeocodeEntry(entry) {\n    const bin = atob(entry.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n    let lat = null, lng = null;\n    for (let i = 0; i + 4 < bin.length && (lat === null || lng === null); i++) {\n      const tag = bin.charCodeAt(i);\n      if (tag !== 0x15 && tag !== 0x1d) continue;\n      let v = 0;\n      for (let j = 3; j >= 0; j--) v = v * 256 + bin.charCodeAt(i + 1 + j); // LE\n      if (v > 0x7fffffff) v -= 0x100000000;\n      if (tag === 0x15) lat = v / 1e6; else lng = v / 1e6;\n      i += 4;\n    }\n    return lat !== null && lng !== null ? { lat, lng } : null;\n  }\n```\n\nVerified against 경복궁 (Gyeongbokgung): `FWFrPQIdEYSRBy...`\n\n→ `37.579617, 126.977041`\n\n. ✅\n\n**Naver** (primary — best transit + English):\n\n```\n  nmap://route/public?dlat={lat}&dlng={lng}&dname={enc}&appname={APPNAME}\n```\n\n`appname`\n\nis `dname`\n\nis optional — omit it and Naver shows the real address. Don't send a literal `Destination`\n\nplaceholder.`route/walk`\n\n, `route/car`\n\n, `route/public`\n\n.**Kakao** (secondary):\n\n```\n  kakaomap://route?ep={lat},{lng}&by=publictransit\n```\n\n`by=foot | car | publictransit`\n\n.**Android:** custom schemes are flaky from Chrome. Use an `intent://`\n\nURL with a built-in store fallback:\n\n```\n  intent://route/public?...#Intent;scheme=nmap;package=com.nhn.android.nmap;S.browser_fallback_url=...;end\n```\n\nFor desktop fallback, Kakao's legacy `link/to`\n\nAPI can't take a start point. But its redirect target can — if you feed it\n\nKakao's internal **WCongnamul** coordinates:\n\n```\n  https://map.kakao.com/?map_type=TYPE_MAP&target=traffic&rt={sx},{sy},{ex},{ey}&rt1={from}&rt2={to}\n```\n\nWCongnamul turned out to be **EPSG:5181 (a GRS80 Transverse Mercator) scaled ×2.5**. I implemented the projection by hand and\n\nit matched Kakao's own conversion to the integer for every test point. (Also: never put a comma in the `rt1/rt2`\n\nlabel — it\n\nbreaks the parser and silently drops the destination.)\n\nThe \"Paste from clipboard\" button worked everywhere except iOS. Two reasons, both subtle:\n\n`text/uri-list`\n\nonly`text/plain`\n\n. So\n`navigator.clipboard.readText()`\n\nreturns an empty string.`await`\n\n. So a `readText()`\n\n→ fall back to `read()`\n\nchain `NotAllowedError`\n\n.The fix is to make **exactly one** clipboard call inside the gesture, then read the type off the already-resolved\n\n`ClipboardItem`\n\n(those `getType`\n\ncalls reuse the granted permission):\n\n``` js\n  const items = await navigator.clipboard.read();   // one call, in the gesture\n  for (const item of items) {\n    for (const type of [\"text/uri-list\", \"text/plain\", \"text/html\"]) {\n      if (!item.types.includes(type)) continue;\n      const text = (await (await item.getType(type)).text()).trim();\n      // uri-list: first non-comment line is the URL\n      if (text) return text;\n    }\n  }\n```\n\nThe iOS \"Paste\" permission bubble itself is unavoidable — it's OS-enforced for any programmatic clipboard read.\n\nGoogle encodes the travel mode in the link (`travelmode=driving`\n\n, `dirflg=d`\n\n, or `!3e0`\n\n). Reading it means a shared *driving*\n\nroute opens directly in driving directions in Naver/Kakao — \"plan in Google Maps, navigate in Korea,\" unchanged.\n\n// 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\" }\n\nIf you're building anything that bridges Google Maps and Korean map apps, steal the deep-link and coordinate logic — that's\n\nexactly why it's public. Questions welcome. 🐣", "url": "https://wpnews.pro/news/routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and", "canonical_source": "https://dev.to/piyaklabs/routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and-ios-clipboard-25mf", "published_at": "2026-06-18 04:50:22+00:00", "updated_at": "2026-06-18 05:21:32.327215+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "machine-learning"], "entities": ["Google Maps", "Naver Map", "KakaoMap", "Cloudflare Worker", "Claude Code", "K-Map Router", "EPSG:5181", "WCongnamul"], "alternates": {"html": "https://wpnews.pro/news/routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and", "markdown": "https://wpnews.pro/news/routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and.md", "text": "https://wpnews.pro/news/routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and.txt", "jsonld": "https://wpnews.pro/news/routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and.jsonld"}}