说实话,我一直觉得“地图”这种基础设施,应该是全球通用的。直到我去了韩国,才第一次被现实打脸:Google Maps在那里连步行和公交导航都给不了——地图数据出口管制,本地人全用Naver和Kakao。游客的常规操作是:从Google Maps复制韩文地名,粘贴到Naver,每个地方重复一遍。谁用谁知道,疼。
所以我顺手做了个小工具 K-Map Router,贴个Google Maps链接就能直接跳到Naver或Kakao的对应路线。代码扔到了 GitHub 上,前后端全在一个 Cloudflare Worker 里,无状态零依赖,免费层就跑得动。整个过程我用了大量 AI 结对编程(Claude Code),但今天不是来秀代码量的——这些东西真正难的地方,是那些几乎没人写清楚的技术细节。如果你也要在韩国的地图生态里折腾,下面这几个坑,能帮你省下好几天。
坐标格式的“多重人格”
你可能会想,解析个坐标有啥难的?Google Maps URL 里直接拿个 lat,lng 不就完了?天真了。
Google Maps 在不同场景下会把坐标藏在完全不同的位置。我按优先级整理了五类:
1. place pin:!3d{lat}!4d{lng},最权威。
2. directions waypoint:!1d{lng}!2d{lat},注意!经度在前!我第一次写正则时理所当然写成 lat,lng,结果返回的坐标跑到海里去了。更坑的是多个 waypoint 时,最后一对才是目的地,第一对是起点——所以如果你要保留 A→B 路线,必须按顺序解析。
3. viewport center:/@{lat},{lng},17z,这个相对友好。
4. query 参数:?query=、&destination=、&daddr=。
5. ll/sll 参数:比较老式。
看似简单,但当你从移动端分享链接时,会发现另一个世界。
移动端“Copy link”里的 protobuf 谜题
如果你在手机上用 Google Maps 分享位置,得到的链接末尾带一个 ?g_st=...。解析后,坐标根本不在明文字段里——它们被编码成了 geocode= 参数,而且是用 base64url 编码的 protobuf 小玩意。
每个 ; 分隔的 entry 编码一个端点。我刚开始直接拿 base64 解码出来是一堆乱码,后来才意识到这是 protobuf 的 fixed32 字段:0x15 表示 field 2(纬度×1e6),0x1D 表示 field 3(经度×1e6),小端序。写了个简单的循环解析,实测对比景福宫坐标 37.579617, 126.977041 完美匹配。
说实话,这种设计对开发者真的很不友好。明明可以明文传个 JSON,偏要套一层二进制。但既然 Google 这么做了,你就得手工 decode。我把这段解码逻辑直接公开了,谁要谁拿去。
Naver 和 Kakao 的深链:表面简单,暗坑无数
Naver 的深链格式大概是:nmap://route/public?dlat={lat}&dlng={lng}&dname={enc}&appname={APPNAME}。看起来不复杂对吧?但你漏掉 appname 就会静默失败——它竟然是个必填参数。而 dname 是可选的,如果不传 Naver 会显示真实地址,千万别传一个“Destination”占位符,否则地图上显示的就是这个字符串。
Kakao 那边则是 kakaomap://route?ep={lat},{lng}&by=publictransit。模式参数 by=foot|car|publictransit,倒是一目了然。
但 Android 上的坑就大了:从 Chrome 里直接调用 custom scheme 会失败。必须用 intent://... URL 带上 fallback 到应用商店的链接。我一开始没加,结果同事的安卓手机点了没反应,查了半天才意识到。
Kakao 的 Web URL 用了一套外星坐标系
最让我崩溃的是 Kakao 的桌面端 fallback。它有一个 legacy 的 /link/to API,但没法传起点。而它的 redirect 目标其实可以接受一个 rt 参数,里面包含起点和终点的坐标——但那坐标不是 WGS84,而是 Kakao 内部用的 WCongnamul 坐标系。
经过实测和反查,这个坐标系其实是 EPSG:5181(GRS80 横轴墨卡托)再乘以 2.5 的缩放。我手工实现了这个投影,逐一验证了 Kakao 自己转换出来的整数结果,全部吻合。其实这就是韩国的官方平面坐标系,只是 Kakao 自己又做了一层缩放,难怪全网几乎找不到文档。有意思的是,如果你在 rt1/rt2 标签里放了逗号,解析器会直接崩掉,静默丢掉目的地——我花了一下午才找到原因。
iOS 剪贴板的“一次性”权限
最后说说一个看着小、实际很恶心的 bug:我加了个“从剪贴板粘贴”按钮,在 Android 和桌面 Chrome 上都正常,但在 iOS Safari 上怎么点都不出结果。
两个原因,都很微妙:
1. Google Maps 的“Copy link”把 URL 放成了 text/uri-list 类型,而不是 text/plain。所以 navigator.clipboard.readText() 返回空字符串。
2. iOS WebKit 在你第一个 await 之后就会撤销用户激活。所以你先尝试 readText() 失败,再 fallback 到 read() 时,第二个调用会因为 NotAllowedError 直接抛出。
解决方法也反直觉:在事件处理函数里只调用一次 navigator.clipboard.read(),然后遍历返回的 ClipboardItem 里的类型(text/uri-list、text/plain、text/html),优先取第一个非注释行。注意,iOS 的“粘贴”权限弹窗是操作系统强制要求的,用户必须手动点允许——这个跑不掉,但至少让逻辑走通了。
一点小建议
如果你也要在韩国地图生态里做类似的事情,我的建议是:别从头造轮子。repo 里的 decodeGeocodeEntry 函数和 WCongnamul 坐标转换逻辑我已经写好、测过、标清楚了注释,直接 copy 过去用就行,比你自己对着 protobuf 文档猜字段号快得多。如果要做二次开发,把坐标解析、深链构造、坐标系转换这些脏活抽成一个纯函数模块,省得跟散落在各处的正则和位运算较劲。
另外,架构上保持无状态会省很多心。一个 Cloudflare Worker 搞定前后端,不存数据库,不记日志,用户复制链接、选地图、跳转,完事。安全性也简单——反正没有任何持久化数据。
最后想吐槽一句:为什么这些大地图公司就不能统一用一个坐标系、一种深链格式?嗯,大概是为了让开发者们有饭吃吧。