# Routing around Google Maps in Korea: Naver & Kakao deep links, weird coordinates, and iOS clipboard

> Source: <https://dev.to/piyaklabs/routing-around-google-maps-in-korea-naver-kakao-deep-links-weird-coordinates-and-ios-clipboard-25mf>
> Published: 2026-06-18 04:50:22+00:00

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. 🐣
