Nominatim이 한국 도로명주소 번지를 못 잡을 때 — 지도 라벨로 좌표를 역산했다

·

블로그 글에 들어갈 위치 지도를 자동 생성하는 스크립트를 쓰고 있다. 공고문에서 주소를 뽑아 좌표로 바꾸고, 그 좌표로 지도 이미지를 만드는 구조다. 주소를 좌표로 바꾸는 단계에서 막혔다.

증상 — 번지가 무시된다

세 개의 실제 주소를 Nominatim에 넣어 봤다.

$ curl -s -G https://nominatim.openstreetmap.org/search \
  --data-urlencode 'q=인천광역시 검단구 독정로 57' \
  --data-urlencode 'format=json' --data-urlencode 'limit=2' \
  -H 'User-Agent: my-app/1.0 (me@example.com)'

결과는 이렇다.

입력 주소돌아온 것type
검단구 독정로 57독정로, 백석동, 당하동tertiary
남동구 서창남순환로 124-14서창남순환로, 서창동tertiary
부평구 주부토로 206주부토로, 부평동tertiary

세 건 모두 도로 자체가 돌아왔다. typetertiarysecondary, 즉 도로 분류다. 건물이 아니다.

문제는 하나의 도로가 여러 세그먼트로 쪼개져 있다는 점이다. 독정로만 해도 좌표가 다른 결과가 셋 나왔고, 이들은 서로 700m 넘게 떨어져 있었다. 번지가 무시되므로 어느 세그먼트를 골라도 실제 건물 위치가 아니다.

구조화 쿼리로 바꿔도 결과는 같았다.

--data-urlencode 'street=57 독정로'
--data-urlencode 'city=검단구'
--data-urlencode 'country=대한민국'

같은 도로 세그먼트들이 돌아왔다. 한국 주소 데이터에 건물 단위 house number가 충분히 들어 있지 않은 것으로 보인다.

국내 지오코딩은 막혀 있었다

당연히 국내 서비스를 먼저 떠올릴 것이다. 나도 그랬는데 쓸 수 없었다. 내 네이버 클라우드 플랫폼 키는 Static Map만 활성화돼 있고 Geocoding API는 403을 반환했다. 지도 이미지는 그릴 수 있는데 주소 변환만 막힌 상태였다.

이 조합이 힌트가 됐다. 좌표 → 이미지는 되니까, 이미지를 보고 좌표를 역으로 맞추면 된다.

아이디어 — 지도에 이미 답이 그려져 있다

핵심은 이것이다. 아파트 단지처럼 이름이 있는 시설은 지도 서비스가 이미 라벨로 표시하고 있다. 내가 찾는 것이 인천검단 LH38단지라면, 지도를 그려 보면 그 이름이 어딘가에 찍혀 나온다.

그러면 문제가 “주소를 좌표로 바꾸기”에서 “라벨이 화면 중앙에 오도록 중심 좌표를 조정하기”로 바뀐다. 후자는 픽셀을 세면 풀 수 있다.

픽셀당 도 단위를 역산하기

이론값을 쓰면 될 것 같지만 그러면 안 된다. 뒤에서 다시 다룬다. 실제로 한 방법은 이렇다.

1단계. 후보 좌표로 한 번 그리고, 찾는 라벨이 이미지의 어느 픽셀에 있는지 읽는다.

중심 A = (37.4279623, 126.7448417)  →  라벨이 (240, 250)에 있음

2단계. 중심을 적당히 옮겨 한 번 더 그린다. 라벨이 어디로 이동했는지 읽는다.

중심 B = (37.43366, 126.73821)  →  라벨이 (570, 580)으로 이동

3단계. 두 결과를 나눠 픽셀당 도 단위를 구한다.

Δ중심 : 위도 +0.005698, 경도 -0.006632
Δ라벨 : (+330 px, +330 px)

경도 1px = 0.006632 / 330 = 2.010e-5 도
위도 1px = 0.005698 / 330 = 1.727e-5 도

방향에 주의해야 한다. 중심 경도를 줄이면 지도 내용은 오른쪽으로 이동한다. 중심을 왼쪽으로 옮긴 만큼 세상이 오른쪽으로 밀려 보이기 때문이다. 위도도 마찬가지로 반대다.

4단계. 라벨을 중앙(384, 384)으로 보내는 데 필요한 중심 이동량을 계산한다.

const PX_LON = 2.010e-5;   // zoom 15, 768px 이미지 기준 실측값
const PX_LAT = 1.727e-5;

function recenter(center, labelXY, size = 768) {
  const c = size / 2;
  const dx = labelXY.x - c;   // 라벨이 중앙에서 얼마나 오른쪽인가
  const dy = labelXY.y - c;   // 얼마나 아래인가
  return {
    lat: center.lat - dy * PX_LAT,   // 라벨이 아래에 있으면 중심을 내린다
    lon: center.lon + dx * PX_LON,   // 오른쪽에 있으면 중심을 오른쪽으로
  };
}

5단계. 새 좌표로 다시 그려서 핀이 라벨 위에 오는지 눈으로 확인한다. 한 번에 맞지 않으면 34단계를 반복한다. 나는 세 단지 모두 23회 만에 맞췄다.

서창1단지 최종 : (37.43027, 126.74195)
검단LH38단지 최종 : (37.5860, 126.68085)
갈산2단지 최종 : (37.51312, 126.72758)

이론값을 쓰면 두 배로 튄다

웹 메르카토르 타일 공식으로 zoom 15의 픽셀당 경도를 계산하면 이렇다.

360 / (256 × 2^15) = 360 / 8,388,608 = 4.291e-5 도/px

그런데 내가 역산한 값은 2.010e-5였다. 약 절반이다.

정확한 이유는 확인하지 못했다. 서비스가 요청한 픽셀 크기를 어떤 배율로 렌더링하는지가 응답에 드러나지 않기 때문이다. 다만 실용적인 결론은 분명하다 — 이론값을 그대로 쓰면 보정이 두 배로 튀어 계속 과보정한다. 실제로 첫 시도에서 한 번 넘어갔다가, 두 렌더링으로 역산한 뒤에야 수렴했다.

이 방식의 장점이 여기 있다. 서비스가 어떤 스케일을 쓰든, 어떤 투영을 쓰든, 결과 이미지 두 장으로 직접 잰 값이라 맞을 수밖에 없다.

자동화할 때 주의할 점

라벨 위치를 읽는 단계가 병목이다. 나는 이미지를 직접 보고 좌표를 읽었다. 완전 자동화하려면 OCR이 필요하고, 그러면 오차가 새로 생긴다. 지도 이미지가 몇 장 안 되는 작업이라면 손으로 읽는 편이 빠르고 정확하다.

라벨이 안 보이면 줌을 올린다. 처음에는 라벨이 핀 라벨 박스에 가려 보이지 않았다. 마커 라벨을 · 한 글자로 줄이거나 줌을 한 단계 올리면 지도의 원래 라벨이 드러난다.

검증을 생략하면 안 된다. 좌표를 추정만 하고 넘어가면 틀린 위치를 그대로 발행하게 된다. 5단계의 눈 확인이 이 방법의 핵심이다. 실제로 나는 첫 보정에서 핀이 고속도로 나들목 위에 앉는 것을 보고 다시 계산했다.

정리

  • Nominatim은 한국 도로명주소의 번지를 해석하지 않고 도로 세그먼트(type: tertiary 등)를 돌려준다.
  • 하나의 도로가 여러 조각으로 나뉘어 있어 어느 것을 골라도 실제 건물이 아니다.
  • 국내 지오코딩이 막혀 있고 지도 렌더링만 가능하다면, 지도에 이미 그려진 라벨을 기준점으로 쓸 수 있다.
  • 중심을 바꿔 두 번 그리면 픽셀당 도 단위를 역산할 수 있다. zoom 15 실측값은 경도 2.010e-5, 위도 1.727e-5였다.
  • 타일 공식의 이론값(4.291e-5)과 약 두 배 차이가 났다. 이론값 대신 역산값을 쓴다.

자주 묻는 질문

구조화 쿼리(street/city)를 쓰면 되지 않나요?

제 경우에는 달라지지 않았습니다. street=57 독정로 형태로 넣어도 같은 도로 세그먼트들이 돌아왔고 번지가 해석되지 않았습니다. Nominatim의 한국 주소 데이터에 건물 단위 house number가 충분히 채워져 있지 않은 것으로 보입니다. 다만 지역과 주소마다 편차가 있으므로 먼저 시도해 보고 결과의 type 필드를 확인하는 편이 좋습니다.

왜 국내 지오코딩 API를 쓰지 않았나요?

쓰려 했지만 제 네이버 클라우드 플랫폼 키는 Static Map만 활성화돼 있고 Geocoding은 403이 떨어졌습니다. 지도 렌더링은 되는데 주소 변환만 막힌 상태였습니다. 국내 지오코딩이 열려 있다면 그쪽이 당연히 정답이고, 이 글의 방법은 그것이 막혔을 때의 우회로입니다.

픽셀당 도 단위를 타일 공식으로 계산하면 안 되나요?

zoom 15에서 360 / (256 × 2^15) = 4.29e-5 도/px이 나오는데, 제가 역산한 값은 2.010e-5로 약 절반이었습니다. 서비스가 요청한 픽셀 크기를 어떤 스케일로 렌더링하는지가 문서에 드러나지 않기 때문에, 이론값을 쓰면 보정이 두 배로 튑니다. 두 번 렌더링해서 역산하는 편이 확실합니다.

핀이 맞았는지는 어떻게 확인했나요?

보정한 좌표로 다시 렌더링해서 핀 마커가 지도에 표시된 단지 라벨 위에 오는지 눈으로 확인했습니다. 세 단지 모두 2~3회 반복해서 맞췄습니다. 좌표를 추정만 하고 넘어가면 틀린 위치를 발행하게 되므로, 렌더링 확인은 생략할 수 없는 단계입니다.

#지오코딩#Nominatim#지도API#자동화#OpenStreetMap