몇 년 이상 주소 데이터를 수집한 모든 조직은 동일한 문제를 가지고 있습니다. 품질을 알 수 없는 크고 지저분한 주소 테이블입니다. 주의를 기울이지 않은 사용자가 웹 폼을 통해 입력한 주소, 입력을 검증하지 않은 레거시 CRM에서 마이그레이션된 레코드, 호환되지 않는 형식의 파트너 Excel 시트에서 대량 가져온 데이터, 데이터 수집 이후 이동한 비즈니스 위치입니다.
나쁜 주소 데이터로 물류를 운영하면 배송 실패가 발생합니다. 이것으로 마케팅을 운영하면 DM 비용이 낭비됩니다. 이것으로 부동산 가치 평가를 실행하면 잘못된 상권 분석이 나옵니다. 이러한 작업이 시작되기 전에 주소를 정제하고 검증해야 합니다.
지오코딩은 이것을 규모 있게 수행하는 가장 효과적인 방법입니다. 원시 주소 문자열을 Geocoding API에 보내면 구조화된 구성요소, 지리 좌표, 신뢰도 점수가 반환됩니다. 높은 신뢰도로 깔끔하게 지오코딩되는 주소는 거의 확실히 유효합니다. 낮은 신뢰도 또는 매칭 없음을 반환하는 주소는 수동 검토가 필요합니다.
이 튜토리얼은 주소 CSV를 읽고, MapAtlas Geocoding API에 대해 각각을 지오코딩하고, 신뢰도 점수가 있는 결과를 쓰고, 낮은 신뢰도 레코드를 수동 검토를 위해 표시하는 완전한 Python 스크립트를 구축합니다. 속도 제한, 일시적 네트워크 오류, 미국 중심 지오코딩 튜토리얼이 놓치는 EU 주소 형식 특성을 처리합니다.
주소 데이터가 나빠지는 이유
주소 품질은 예측 가능한 이유로 저하됩니다.
수동 입력 오류. 웹 폼 사용자는 빠르게 입력하고, 자동 수정이 거리 이름을 손상시키며, 비어 있지 않은 문자열을 수락하는 검증은 쓰레기를 통과시킵니다. B2C 결제 데이터 연구에서 수동으로 입력된 주소의 7-12%가 배송 실패를 일으킬 만큼 충분한 오류를 포함한다는 것을 발견했습니다.
비즈니스 이전. 2년 전에 수집된 B2B 데이터베이스는 이동, 합병, 폐업으로 인해 약 10-15%의 주소가 더 이상 현재 비즈니스 위치와 일치하지 않을 것입니다.
형식 불일치. 여러 소스에서 수집된 데이터는 다른 관행을 사용합니다. 전체 국가 이름 vs. ISO 코드, "St." vs. "Street", 거리 앞 번지수 vs. 뒤, "flat" vs. "apt" vs. "wohnung". 지오코더는 이것들을 구조화된 출력으로 정규화합니다.
레거시 시스템 마이그레이션. 여러 데이터베이스 열에 분할된 주소 필드는 마이그레이션 중에 종종 연결되어 구조를 잃습니다. 오래된 시스템의 자유 텍스트 주소 필드에는 실제 주소의 일부가 아닌 메모, 참조 또는 형식이 포함될 수 있습니다.
지오코딩 접근 방식은 문자열 매칭이 아닌 지리적 매칭을 사용하기 때문에 이 모든 것을 처리합니다. 잘못 형식화된 주소도 기본 위치 데이터가 일치하면 올바르게 지오코딩될 수 있습니다.
신뢰도 점수 이해하기
MapAtlas Geocoding API는 각 피처에 0.0에서 1.0 범위의 confidence 속성을 반환합니다. 형식 차이, 약어, 모호성을 고려하여 반환된 결과가 입력 쿼리와 얼마나 밀접하게 일치하는지를 나타냅니다.
| 신뢰도 | 해석 | 권장 조치 |
|---|---|---|
| 0.90 - 1.00 | 정확한 또는 근사치 매칭 | 자동 수락 |
| 0.85 - 0.89 | 강한 매칭, 사소한 형식 차이 | 로깅과 함께 수락 |
| 0.60 - 0.84 | 부분 매칭, 거리는 찾았지만 번지수 불확실 | 수동 검토 표시 |
| 0.40 - 0.59 | 모호, 지역은 매칭되었지만 특정 주소 아님 | 거부 또는 에스컬레이션 |
| 0.00 - 0.39 | 의미 있는 매칭 없음 | 거부, 아마도 유효하지 않음 |
이 임계값은 시작점입니다. 배송 실패가 비싼 물류 운영의 경우 자동 수락 임계값을 0.92+로 높이세요. 몇 가지 나쁜 주소 비용이 수동 검토 비용을 초과하는 마케팅 메일링 리스트의 경우 0.80으로 낮추세요.
API는 또한 매칭된 주소 계층 수준을 나타내는 match_type 속성을 반환합니다. point(정확한 건물), interpolated(알려진 번지수 사이에서 추정된 위치), street(거리는 찾았지만 번지수 없음), locality(도시만 매칭됨)입니다.
Python 검증 스크립트
의존성 설치:
pip install requests pandas tqdm
스크립트는 address 열(또는 별도의 street, city, country 열)이 있는 CSV를 읽고, 각 행을 지오코딩하고, 검증 결과가 추가된 새 CSV를 씁니다.
#!/usr/bin/env python3
"""
bulk_geocode.py, Validate a CSV of addresses using the MapAtlas Geocoding API.
Input CSV must have either:
- An 'address' column (full address string), or
- 'street', 'city', and 'country' columns (will be concatenated)
Outputs a new CSV with added columns:
geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
"""
import csv
import time
import logging
import requests
import pandas as pd
from tqdm import tqdm
from pathlib import Path
# ── Configuration ──────────────────────────────────────────────────────────────
API_KEY = 'YOUR_API_KEY'
API_BASE = 'https://api.mapatlas.eu/geocoding/v1/search'
INPUT_CSV = 'addresses.csv'
OUTPUT_CSV = 'addresses_validated.csv'
RATE_LIMIT_RPS = 5 # Requests per second (stay within your plan limits)
RETRY_ATTEMPTS = 3 # Retries on transient errors
RETRY_DELAY_S = 2.0 # Seconds between retries
# Confidence thresholds
ACCEPT_THRESHOLD = 0.85
REVIEW_THRESHOLD = 0.60
logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
log = logging.getLogger(__name__)
# ── Geocoding function ─────────────────────────────────────────────────────────
def geocode_address(address_str: str) -> dict:
"""
Geocode a single address string. Returns a dict with result fields.
Retries on network errors and 429 rate-limit responses.
"""
params = {
'text': address_str,
'key': API_KEY,
'size': 1,
}
for attempt in range(RETRY_ATTEMPTS):
try:
resp = requests.get(API_BASE, params=params, timeout=10)
if resp.status_code == 429:
# Rate limited, wait and retry
wait = float(resp.headers.get('Retry-After', RETRY_DELAY_S * (attempt + 1)))
log.warning(f'Rate limited. Waiting {wait:.1f}s before retry {attempt + 1}.')
time.sleep(wait)
continue
resp.raise_for_status()
data = resp.json()
if not data.get('features'):
return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
'confidence': 0.0, 'match_type': 'no_match', 'status': 'reject'}
feature = data['features'][0]
props = feature['properties']
coords = feature['geometry']['coordinates'] # [lng, lat]
confidence = round(float(props.get('confidence', 0.0)), 4)
match_type = props.get('match_type', 'unknown')
if confidence >= ACCEPT_THRESHOLD:
status = 'accept'
elif confidence >= REVIEW_THRESHOLD:
status = 'review'
else:
status = 'reject'
return {
'geocoded_label': props.get('label', ''),
'geocoded_lat': round(coords[1], 6),
'geocoded_lng': round(coords[0], 6),
'confidence': confidence,
'match_type': match_type,
'status': status,
}
except requests.exceptions.RequestException as exc:
log.warning(f'Network error on attempt {attempt + 1}: {exc}')
if attempt < RETRY_ATTEMPTS - 1:
time.sleep(RETRY_DELAY_S * (attempt + 1))
# All retries exhausted
return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
'confidence': 0.0, 'match_type': 'error', 'status': 'error'}
# ── Main processing loop ───────────────────────────────────────────────────────
def main():
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
# Build address string from available columns
if 'address' in df.columns:
df['_query'] = df['address']
elif all(c in df.columns for c in ['street', 'city', 'country']):
df['_query'] = df['street'] + ', ' + df['city'] + ', ' + df['country']
else:
raise ValueError("CSV must have 'address' or 'street'+'city'+'country' columns.")
results = []
sleep_interval = 1.0 / RATE_LIMIT_RPS
for query in tqdm(df['_query'], desc='Geocoding', unit='addr'):
result = geocode_address(query.strip())
results.append(result)
time.sleep(sleep_interval)
results_df = pd.DataFrame(results)
output_df = pd.concat([df.drop(columns=['_query']), results_df], axis=1)
output_df.to_csv(OUTPUT_CSV, index=False, quoting=csv.QUOTE_NONNUMERIC)
# Summary
total = len(output_df)
accept = (output_df['status'] == 'accept').sum()
review = (output_df['status'] == 'review').sum()
reject = (output_df['status'] == 'reject').sum()
errors = (output_df['status'] == 'error').sum()
log.info(f'\n── Results ────────────────────────────────')
log.info(f'Total processed : {total:,}')
log.info(f'Accept (≥{ACCEPT_THRESHOLD}) : {accept:,} ({accept/total:.1%})')
log.info(f'Review : {review:,} ({review/total:.1%})')
log.info(f'Reject : {reject:,} ({reject/total:.1%})')
log.info(f'Errors : {errors:,} ({errors/total:.1%})')
log.info(f'Output written to {OUTPUT_CSV}')
if __name__ == '__main__':
main()
실행:
python bulk_geocode.py
초당 5개 요청으로 10,000개 주소 CSV를 처리하는 데 약 35분이 걸립니다. (tqdm을 통한) 진행 표시줄은 실시간 처리량과 예상 완료 시간을 보여줍니다.
출력 처리
출력 CSV는 입력 데이터에 6개의 열을 추가합니다.
address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
상태별로 필터링하여 세 가지 출력 파일을 생성하세요.
# After running main(), split into acceptance tiers:
df = pd.read_csv('addresses_validated.csv')
df[df['status'] == 'accept'].to_csv('addresses_clean.csv', index=False)
df[df['status'] == 'review'].to_csv('addresses_review.csv', index=False)
df[df['status'] == 'reject'].to_csv('addresses_reject.csv', index=False)
print(f"Clean: {len(df[df['status']=='accept']):,} addresses ready for use")
print(f"Review: {len(df[df['status']=='review']):,} addresses for manual check")
print(f"Reject: {len(df[df['status']=='reject']):,} addresses to discard or fix")
addresses_review.csv 파일은 사람의 주의가 필요한 파일입니다. 일반적인 검토 패턴:
- 매칭 유형
street(신뢰도 0.65-0.80): 거리는 찾았지만 번지수는 찾지 못했습니다. 새 건물, 커버리지가 희박한 농촌 주소, 또는 번지수 오타일 가능성이 높습니다. 원본 소스를 확인하세요. - 매칭 유형
locality(신뢰도 0.50-0.65): 도시만 매칭되었습니다. 거리 이름의 철자가 잘못되었거나 데이터에 존재하지 않을 가능성이 높습니다. 우편 디렉토리에서 주소를 조회하세요. - 명확히 유효해 보이는 주소의 낮은 신뢰도: 국가/언어 불일치를 확인하세요. 국가 제한 없이 쿼리된 네덜란드 주소는 비슷한 이름의 독일 또는 벨기에 도시와 지오코딩될 수 있습니다.
알아야 할 EU 주소 특성
미국 시장을 위해 작성된 대량 지오코딩 가이드는 EU 데이터셋을 어렵게 만드는 형식 차이를 건너뜁니다. 가장 일반적인 문제들입니다.
독일, 번지수 순서. 독일 주소는 {거리} {번호} 형식을 사용합니다: Berliner Straße 42. 많은 CRM은 UK/US 관행으로 구축되었기 때문에 {번호} {거리} 형식으로 주소를 저장합니다. 독일 주소가 낮은 신뢰도로 지오코딩되는 경우 제출 전에 쿼리 문자열에서 번호와 거리 이름을 반대로 해보세요.
프랑스, 아롱디스망. 파리 주소는 우편번호의 일부로 아롱디스망(1-20)을 포함합니다: 75001에서 75020. 아롱디스망을 생략하고 단순히 Paris를 사용하는 쿼리는 특정 구역이 아닌 도시 중심점으로 지오코딩됩니다. 이것은 address 매칭이 아닌 낮은 신뢰도의 locality 매칭으로 나타납니다.
네덜란드, 우편번호 형식. 네덜란드 우편번호는 엄격한 DDDD LL 패턴을 따릅니다(4자리, 공백, 2개의 대문자). 공백 없이 저장된 코드(1012 LG 대신 1012LG) 또는 소문자는 올바르게 지오코딩됩니다. 하지만 우편번호 형식을 별도로 검증하는 경우 공백과 함께 대문자로 정규화하세요.
벨기에, 언어 모호성. 일부 벨기에 지자체에는 다른 프랑스어와 네덜란드어 이름이 있습니다(Liège/Luik, Gent/Gand). API는 둘 다 처리하지만 데이터셋의 일치하지 않는 이름 지정(일부 레코드는 프랑스어, 일부는 네덜란드어)은 다른 신뢰도 수준을 만들 수 있습니다. 지오코딩 전에 지역별 하나의 언어로 정규화하세요.
스페인 및 이탈리아, 거리 접두사 변형. "Calle", "Carrer", "Via", "Viale"은 모두 유효한 거리 유형 접두사이며 레코드에 약어("C/")로 나타날 수 있습니다. 지오코더는 일반적인 약어를 처리하지만 레거시 시스템의 흔치 않은 단축형은 정규화 단계가 필요할 수 있습니다.
동시 요청으로 처리량 높이기
순차 스크립트는 안전하고 간단하지만 느립니다. 더 큰 데이터셋의 경우 순차 루프를 동시 실행기로 교체하세요.
from concurrent.futures import ThreadPoolExecutor, as_completed
def main_concurrent(max_workers=10):
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
# ... (same setup as before) ...
results = [None] * len(df)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_idx = {
executor.submit(geocode_address, row['_query'].strip()): idx
for idx, row in df.iterrows()
}
for future in tqdm(as_completed(future_to_idx), total=len(df), desc='Geocoding'):
idx = future_to_idx[future]
results[idx] = future.result()
# ... (same output writing as before) ...
워커당 5 RPS로 10 동시 워커를 사용하면 처리량이 초당 약 50개의 요청에 도달합니다. 10,000개의 주소를 4분 미만에 처리합니다. max_workers를 늘리기 전에 MapAtlas 플랜의 동시 요청 제한을 확인하세요.
주소 검증을 반복 작업으로 실행하는 조직(새 CRM 가져오기를 매주 정리하거나 배송 주소를 밤마다 검증)은 MapAtlas 통합이 운영 워크플로에 어떻게 맞는지에 대해 물류 및 배송 솔루션 페이지를 참조하세요.
나쁜 데이터가 시스템에 처음부터 입력되는 것을 방지하기 위해 주소 자동완성을 구축하고 있다면(배치로 정제하는 것이 아닌 소스에서 오류 포착), 주소 자동완성 API: 결제 전환율 35% 향상에서 프론트엔드 구현을 다룹니다.
좌표 출력 활용하기
검증된 출력에는 수락된 모든 주소의 geocoded_lat 및 geocoded_lng가 포함됩니다. 이 좌표는 원시 주소 문자열로는 불가능했던 분석 기능을 열어줍니다.
- 거리 계산. 창고와 각 배송 주소 사이의 직선 거리를 계산하여 배송 비용 등급을 추정합니다.
- 상권 분석. 검증된 고객 위치를 지도에 표시하여 수요가 지리적으로 어디에 집중되는지 확인합니다.
- 배송 구역 할당. 좌표가 구역 폴리곤 내에 있는지 테스트하여 각 주소를 배송 구역에 할당합니다.
- 중복 탐지. 동일한 좌표(몇 미터 내)를 가진 두 레코드는 주소 문자열이 형식이 다르더라도 중복일 가능성이 높습니다.
NAP(이름/주소/전화번호) 일관성 및 AI 검색 가시성에 미치는 영향, 특히 로컬 비즈니스 주소 데이터베이스와 관련하여, AI 검색을 위한 NAP 일관성: 일치하지 않는 주소가 ChatGPT 가시성을 죽이는 이유는 지오코딩 검증된 주소가 구조화된 데이터 markup의 올바른 기반인 이유를 설명합니다.
요약
MapAtlas Geocoding API를 사용한 대량 지오코딩은 다음을 제공합니다.
- 검증된 주소 - 신뢰도 점수가 있어 자동으로 신뢰할 레코드와 검토가 필요한 레코드를 알 수 있습니다.
- 구조화된 구성요소 (거리, 번지수, 우편번호, 도시, 국가) - 모든 입력 형식에서 정규화됩니다.
- 지리 좌표 - 수락된 모든 주소에 대해, 공간 분석 및 라우팅이 가능합니다.
- EU 주소 형식 지원 - API에 내장되어 전처리에서 처리할 필요가 없습니다.
Python 스크립트는 순차적으로 초당 5개 주소, 동시 워커로 최대 초당 50개로 실행됩니다. 10,000개의 주소는 동시성에 따라 4-40분이 소요됩니다. 출력은 수락, 검토, 거부의 세 가지 등급으로 깔끔한 CSV입니다.
무료 MapAtlas API 키 신청으로 시작하세요. Geocoding API는 모든 플랜에서 대량 쿼리를 지원합니다. 요청당 요금과 월간 무료 티어 한도는 가격을 참조하세요.
자주 묻는 질문
MapAtlas API로 주소 10,000건을 지오코딩하는 데 얼마나 걸리나요?
작은 수면 버퍼와 함께 초당 5개 요청의 보수적인 속도로 10,000개의 주소는 약 35-40분이 걸립니다. 동시 요청(10 워커)과 적절한 속도 제한을 사용하면 5분 미만으로 줄어듭니다. 이 튜토리얼의 Python 스크립트는 순차 및 동시 옵션을 모두 포함합니다.
주소 검증에 어떤 신뢰도 점수 임계값을 사용해야 하나요?
실용적인 3단계 시스템이 잘 작동합니다. 0.85 이상의 신뢰도는 수락, 0.60에서 0.85 사이는 수동 검토를 위해 표시, 0.60 미만은 거부합니다. 정확한 임계값은 사용 사례에 따라 다릅니다. 물류 운영은 마케팅 캠페인보다 더 엄격한 임계값을 사용해야 합니다.
MapAtlas Geocoding API가 EU 특정 주소 형식을 처리하나요?
네. API는 독일(거리 다음 번지수), 프랑스(거리 앞 번지수), 네덜란드(4+2 우편번호) 및 기타 EU 국가 주소 관행을 올바르게 파싱합니다. 현지 언어 쿼리는 영어 형식 쿼리보다 더 좋은 결과를 반환합니다.

