"독도 날씨"가 전남 고흥 관측소로 잡히길래 파봤더니 한 건이 아니었다.
KR_CITY_COORDS에 없는 ASOS 지명 66개를 기상청 공식 좌표와 대조한 결과
44개가 틀렸다 — 25개는 Open-Meteo 지오코더가 아예 못 찾고, 19개는
엉뚱한 좌표를 줬다.
홍성 724km / 고산 629km / 보령 551km / 밀양 459km / 서산 / 홍천
→ 전부 북한 동명 지역
동해 408km / 남원 246km / 제천 214km / 상주 188km / 남해 187km
→ 사람들이 실제로 묻는 도시들
이제 ASOS 97 + AWS 745 지점 이름표를 지오코딩 소스로 쓴다. 순서는
손으로 넣은 표 → 지점 이름표 → 지오코더이고, 한글이 섞인 입력만
이름표를 탄다(영문·해외 지명은 종전 경로 그대로). 지점 좌표는 기상청
발표값이라 이 문제가 구조적으로 안 생기고, 날씨를 묻는 맥락에서는
"그 지명의 관측소 위치"가 사실상 정답이다.
고친 뒤 ASOS 97개 전부 공식 좌표 20km 이내로 들어왔다(수정 전 53/97).
같은 이름이 100km 넘게 떨어져 둘씩 있는 3건(진안·옥천·가산)은 이름표에서
일부러 제외했다 — 조용히 한쪽을 고르면 틀렸을 때 드러나지 않는다. 대신
널리 통하는 행정구역 쪽을 KR_CITY_COORDS에 못박았다. 안 그러면 넘어간
지오코더가 옥천을 북한(39.94, 124.53)으로 보낸다. 버린 쪽은 전부 경기도의
동명 지점(진안 439, 옥천 449, 가산 473).
미해결로 남긴 것: 'New York'이 네브래스카의 York로 간다. 영문·해외 지명
쪽 지오코더 문제라 이번 변경 경로와 무관하다(한글 없는 입력은 이름표를
안 탄다).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
3.9 KiB
TypeScript
89 lines
3.9 KiB
TypeScript
/**
|
|
* korean-place-lookup.test.ts
|
|
*
|
|
* 한국 지명 → 좌표를 기상청 관측지점 이름표(ASOS 97 + AWS 745)로 푼다.
|
|
*
|
|
* 계기는 "독도 날씨"가 전남 고흥 관측소로 잡힌 것이었는데, 파보니 한 건이 아니었다.
|
|
* KR_CITY_COORDS에 없는 ASOS 지명 66개를 공식 좌표와 대조하니 **44개가 틀렸다** —
|
|
* 25개는 Open-Meteo 지오코더가 아예 못 찾고, 19개는 엉뚱한 좌표를 줬다:
|
|
*
|
|
* 홍성 724km / 고산 629km / 보령 551km / 밀양 459km → 전부 북한 동명 지역
|
|
* 동해 408km · 제천 214km · 상주 188km · 남원 246km → 사람들이 실제로 묻는 도시들
|
|
*
|
|
* 지점 좌표는 기상청 발표값이라 이 문제가 구조적으로 안 생기고, 날씨 맥락에선
|
|
* "그 지명의 관측소 위치"가 사실상 정답이다. 고친 뒤 ASOS 97개 전부 20km 이내로 들어왔다.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { lookupKoreanPlace } from '../src/tools/weather';
|
|
|
|
// 기상청 공식 좌표(stn_inf.php)와의 거리
|
|
function km(a: { lat: number; lon: number }, lat: number, lon: number): number {
|
|
const R = (d: number) => d * Math.PI / 180;
|
|
const dLat = R(lat - a.lat), dLon = R(lon - a.lon);
|
|
const q = Math.sin(dLat / 2) ** 2 + Math.cos(R(a.lat)) * Math.cos(R(lat)) * Math.sin(dLon / 2) ** 2;
|
|
return 6371 * 2 * Math.asin(Math.sqrt(q));
|
|
}
|
|
|
|
describe('lookupKoreanPlace — 지점 이름표로 한국 지명 풀기', () => {
|
|
test('지오코더가 북한으로 보내던 지명들이 제자리에 온다', () => {
|
|
// 좌: 공식 관측지점 좌표. 우: 예전 지오코더가 주던 곳.
|
|
const cases: Array<[string, number, number]> = [
|
|
['홍성', 36.6576, 126.6877], // → 42.71, 129.83 (조선민주주의인민공화국)
|
|
['보령', 36.3272, 126.5574], // → 40.88, 129.08
|
|
['밀양', 35.4915, 128.7441], // → 38.85, 125.75
|
|
];
|
|
for (const [name, lat, lon] of cases) {
|
|
const got = lookupKoreanPlace(name);
|
|
assert.ok(got, `${name} 조회 실패`);
|
|
assert.ok(km(got, lat, lon) < 5, `${name} ${km(got, lat, lon).toFixed(0)}km 어긋남`);
|
|
}
|
|
});
|
|
|
|
test('수백 km 어긋나던 도시들', () => {
|
|
const cases: Array<[string, number, number]> = [
|
|
['동해', 37.5071, 129.1243], // 예전 408km
|
|
['제천', 37.1593, 128.1943], // 예전 214km
|
|
['남원', 35.4213, 127.3965], // 예전 246km
|
|
['남해', 34.8166, 127.9264], // 예전 187km
|
|
];
|
|
for (const [name, lat, lon] of cases) {
|
|
const got = lookupKoreanPlace(name);
|
|
assert.ok(got && km(got, lat, lon) < 5, `${name} 어긋남`);
|
|
}
|
|
});
|
|
|
|
test('독도 — 이 작업의 발단', () => {
|
|
// AWS 96번 독도가 실재하는데 지오코더는 전남을 줬다.
|
|
const got = lookupKoreanPlace('독도');
|
|
assert.ok(got);
|
|
assert.ok(km(got, 37.2395, 131.8698) < 5);
|
|
});
|
|
|
|
test('지오코더가 아예 못 찾던 이름들도 잡힌다', () => {
|
|
for (const n of ['흑산도', '태백', '보은', '의성', '산청', '정읍']) {
|
|
assert.ok(lookupKoreanPlace(n), `${n} 조회 실패`);
|
|
}
|
|
});
|
|
|
|
test('행정 접미사를 붙여도 같은 곳', () => {
|
|
const a = lookupKoreanPlace('양산');
|
|
const b = lookupKoreanPlace('양산시');
|
|
assert.deepEqual(a, b);
|
|
});
|
|
|
|
test('멀리 떨어진 동명은 쓰지 않는다 (조용히 한쪽을 고르면 안 됨)', () => {
|
|
// 진안·옥천·가산은 100km 넘게 떨어진 동명 지점이 둘씩 있다. 이름표는 넘기고,
|
|
// 널리 통하는 행정구역 쪽은 KR_CITY_COORDS가 따로 못박는다.
|
|
for (const n of ['진안', '옥천', '가산']) {
|
|
assert.equal(lookupKoreanPlace(n), null, `${n}은 모호로 걸러져야 한다`);
|
|
}
|
|
});
|
|
|
|
test('한글이 아닌 이름은 이름표에 없다', () => {
|
|
assert.equal(lookupKoreanPlace('London'), null);
|
|
assert.equal(lookupKoreanPlace(''), null);
|
|
});
|
|
});
|