address - Vietnamese Address Converter
Chuyển đổi địa chỉ Việt Nam từ hệ thống hành chính cũ (63 tỉnh/thành phố, 3 cấp) sang hệ thống mới sau sáp nhập (34 tỉnh/thành phố, 2 cấp), có hiệu lực từ 01/07/2025.
Technical Report
1. Bối cảnh
Nghị quyết của Quốc hội về sáp nhập đơn vị hành chính có hiệu lực ngày 01/07/2025, thay đổi cấu trúc hành chính Việt Nam:
| Cũ | Mới | |
|---|---|---|
| Tỉnh/Thành phố | 63 | 34 |
| Cấp hành chính | Tỉnh > Huyện > Xã | Tỉnh > Xã |
| Tổng số xã/phường | ~10,600 | ~3,300 |
Cấp huyện (quận/huyện/thị xã/thành phố trực thuộc tỉnh) bị loại bỏ hoàn toàn khỏi địa chỉ hành chính. Các xã/phường được sáp nhập, đổi tên, chia tách hoặc giữ nguyên tùy khu vực.
2. Kiến trúc hệ thống
address/
├── pyproject.toml # Package config (uv + hatchling)
├── data/
│ └── mapping.json # 10,602 bản ghi mapping (4.7 MB)
├── src/
│ ├── __init__.py # Public API
│ ├── models.py # Data models & enums
│ ├── normalizer.py # Chuẩn hóa tiếng Việt
│ ├── parser.py # Phân tích chuỗi địa chỉ
│ ├── converter.py # Logic chuyển đổi chính
│ └── cli.py # CLI (click)
├── scripts/
│ └── build_mapping.py # Tạo mapping.json từ vietnamadminunits
└── tests/
└── test_converter.py # 13 test cases
Nguyên tắc thiết kế: Package hoạt động standalone — dữ liệu mapping được extract một lần từ vietnamadminunits (dev dependency) vào data/mapping.json, sau đó runtime chỉ cần click là dependency duy nhất.
3. Nguồn dữ liệu
Dữ liệu mapping được extract từ package vietnamadminunits v1.0.4 (PyPI, MIT license) thông qua script scripts/build_mapping.py. Package này cung cấp 3 file JSON chính:
| File | Nội dung | Kích thước |
|---|---|---|
converter_2025.json |
Mapping cũ → mới (province + ward) | 596 KB |
parser_legacy.json |
Dữ liệu hành chính cũ (63 tỉnh, 3 cấp) | 2.6 MB |
parser_from_2025.json |
Dữ liệu hành chính mới (34 tỉnh, 2 cấp) | 957 KB |
Cấu trúc dữ liệu gốc (trong converter_2025.json):
DICT_PROVINCE:{new_province_key: [old_province_key_1, old_province_key_2, ...]}— mapping tỉnhDICT_PROVINCE_WARD_NO_DIVIDED:{new_prov: {new_ward: [old_compound_key, ...]}}— xã không bị chia táchDICT_PROVINCE_WARD_DIVIDED:{new_prov: {old_compound_key: [{newWardKey, isDefaultNewWard, ...}]}}— xã bị chia tách
Compound key format: {province_key}_{district_key}_{ward_key} (ví dụ: thanhphohanoi_quanbadinh_phuongphucxa).
4. Build mapping (scripts/build_mapping.py)
Script extract và chuẩn hóa dữ liệu từ 3 file JSON của vietnamadminunits thành một file data/mapping.json thống nhất:
uv run python scripts/build_mapping.py
Output mapping.json chứa:
| Trường | Mô tả |
|---|---|
metadata |
Source, version, effective_date, thống kê |
province_mapping |
{old_key: new_key} — 63 entries |
province_names |
{key: {name, short, code}} — 34 tỉnh mới |
old_province_names |
{key: {name, short, code}} — 63 tỉnh cũ |
ward_mapping |
List 10,602 records chi tiết |
Mỗi ward mapping record gồm:
{
"old_province": "Thành phố Hà Nội",
"old_province_key": "thanhphohanoi",
"old_district": "Quận Ba Đình",
"old_district_key": "quanbadinh",
"old_ward": "Phường Phúc Xá",
"old_ward_key": "phuongphucxa",
"new_province": "Thành phố Hà Nội",
"new_province_key": "thanhphohanoi",
"new_ward": "Phường Hồng Hà",
"new_ward_key": "phuonghongha",
"mapping_type": "merged"
}
Phân loại mapping type:
| Type | Số lượng | Mô tả |
|---|---|---|
unchanged |
149 | Xã giữ nguyên tên |
renamed |
92 | Xã đổi tên (1:1) |
merged |
9,328 | Nhiều xã cũ gộp thành 1 xã mới |
divided |
1,033 | 1 xã cũ chia thành nhiều xã mới |
| Tổng | 10,602 |
Logic phân loại:
unchanged: Chỉ có 1 compound key cũ trỏ vào ward mới VÀ tên trùng nhaurenamed: Chỉ có 1 compound key cũ trỏ vào ward mới VÀ tên khác nhaumerged: Nhiều compound key cũ cùng trỏ vào 1 ward mớidivided: TừDICT_PROVINCE_WARD_DIVIDED, cóis_defaultflag cho mỗi option
5. Modules
5.1 src/models.py — Data Models
MappingType(str, Enum):UNCHANGED,RENAMED,MERGED,DIVIDEDConversionStatus(str, Enum):SUCCESS,PARTIAL,NOT_FOUNDAdminUnit(@dataclass):province,district,ward,street+to_address()ConversionResult(@dataclass):original,converted,status,mapping_type,old,new,note
5.2 src/normalizer.py — Chuẩn hóa tiếng Việt
Ba hàm chính:
| Hàm | Input | Output | Mô tả |
|---|---|---|---|
remove_diacritics() |
"Phường Phúc Xá" |
"Phuong Phuc Xa" |
Unicode NFKD decomposition + xử lý đ/Đ |
normalize_key() |
"Quận Ba Đình" |
"quanbadinh" |
Lowercase + bỏ dấu + bỏ space/punctuation |
expand_abbreviations() |
"P.Phúc Xá, Q.Ba Đình" |
"phường phúc xá, quận ba đình" |
Mở rộng viết tắt |
Bảng viết tắt hỗ trợ:
| Viết tắt | Đầy đủ |
|---|---|
TP., T.P. |
Thành phố |
P. |
Phường |
Q. |
Quận |
H. |
Huyện |
TX., T.X. |
Thị xã |
TT., T.T. |
Thị trấn |
X. |
Xã |
5.3 src/parser.py — Phân tích địa chỉ
Parse chuỗi địa chỉ theo format "street, ward, district, province" bằng phương pháp right-to-left positional assignment:
"123 Hàng Bông, Phường Phúc Xá, Quận Ba Đình, TP Hà Nội"
↑ ↑ ↑ ↑
street ward district province
(parts[:-3]) (parts[-3]) (parts[-2]) (parts[-1])
Xử lý đặc biệt cho địa chỉ 2 phần ("ward, province"): kiểm tra prefix phường/xã/thị trấn để phân biệt với district.
5.4 src/converter.py — Logic chuyển đổi
Khởi tạo (lazy load):
- Load
data/mapping.jsonlần đầu khi gọiconvert_address() - Build index một lần, cache trong module-level globals
Index structure:
| Index | Key | Value | Mục đích |
|---|---|---|---|
province |
old_prov_key |
new_prov_key |
Tra cứu tỉnh |
province_keywords |
normalized_name |
old_prov_key |
Fuzzy match tên tỉnh |
exact |
(prov, dist, ward) |
[records] |
Tra cứu chính xác |
ward_only |
(prov, ward) |
[records] |
Tra cứu bỏ qua quận/huyện |
Luồng chuyển đổi (convert_address):
Input: "Phường Phúc Xá, Quận Ba Đình, Thành phố Hà Nội"
│
├─ 1. parse_address() → AdminUnit(ward, district, province)
│
├─ 2. Resolve province
│ normalize("Thành phố Hà Nội") → "thanhphohanoi"
│ province_mapping["thanhphohanoi"] → "thanhphohanoi"
│ ❌ Không tìm thấy → NOT_FOUND
│
├─ 3. Tra cứu ward (2-tier matching)
│ ├─ Tier 1: exact("thanhphohanoi", "quanbadinh", "phuongphucxa") ✅
│ └─ Tier 2: ward_only("thanhphohanoi", "phuongphucxa") (fallback)
│
├─ 4. Chọn bản ghi tốt nhất
│ └─ Nếu divided: ưu tiên is_default=true
│
└─ 5. Build result
ConversionResult(converted="Phường Hồng Hà, Thành phố Hà Nội",
status=SUCCESS, mapping_type=MERGED)
Conversion status:
| Status | Điều kiện |
|---|---|
SUCCESS |
Tìm thấy cả province + ward mapping |
PARTIAL |
Chỉ tìm thấy province (ward không match) |
NOT_FOUND |
Không nhận dạng được province |
5.5 src/cli.py — Command Line Interface
Hai commands, sử dụng click:
# Chuyển đổi 1 địa chỉ
address-convert convert "Phường Phúc Xá, Quận Ba Đình, TP Hà Nội"
# Chuyển đổi hàng loạt từ CSV
address-convert batch input.csv output.csv --column address
Batch mode đọc CSV, thêm 3 cột vào output: converted_address, conversion_status, mapping_type.
6. Test Results
13 test cases, tất cả pass:
tests/test_converter.py::TestMergedWard::test_phuc_xa_merged_to_hong_ha PASSED
tests/test_converter.py::TestMergedWard::test_an_binh_can_tho PASSED
tests/test_converter.py::TestUnchangedWard::test_tan_loc_can_tho PASSED
tests/test_converter.py::TestRenamedWard::test_long_hoa_renamed PASSED
tests/test_converter.py::TestDividedWard::test_divided_selects_default PASSED
tests/test_converter.py::TestAbbreviations::test_p_q_tp PASSED
tests/test_converter.py::TestAbbreviations::test_tp_shorthand PASSED
tests/test_converter.py::TestPartialAddress::test_province_only PASSED
tests/test_converter.py::TestPartialAddress::test_unknown_province PASSED
tests/test_converter.py::TestWithStreet::test_street_preserved PASSED
tests/test_converter.py::TestBatchConvert::test_batch PASSED
tests/test_converter.py::TestProvinceMapping::test_ha_noi_stays PASSED
tests/test_converter.py::TestProvinceMapping::test_merged_province PASSED
Coverage theo loại test:
| Nhóm test | Kiểm tra |
|---|---|
| Merged ward | Nhiều xã cũ → 1 xã mới (Phúc Xá → Hồng Hà) |
| Unchanged ward | Xã giữ nguyên (Tân Lộc) |
| Renamed ward | Xã đổi tên (Long Hòa → Long Tuyền) |
| Divided ward | Xã chia tách, chọn default |
| Abbreviations | P., Q., TP. mở rộng đúng |
| Partial address | Chỉ tỉnh, tỉnh không tồn tại |
| Street preserved | Giữ nguyên phần đường khi chuyển đổi |
| Batch convert | Chuyển đổi hàng loạt, mixed results |
| Province mapping | Tỉnh giữ nguyên, tỉnh sáp nhập |
7. Danh sách 34 tỉnh/thành phố mới
| Mã | Tên |
|---|---|
| 01 | Thành phố Hà Nội |
| 04 | Tỉnh Cao Bằng |
| 08 | Tỉnh Tuyên Quang |
| 11 | Tỉnh Điện Biên |
| 12 | Tỉnh Lai Châu |
| 14 | Tỉnh Sơn La |
| 15 | Tỉnh Lào Cai |
| 19 | Tỉnh Thái Nguyên |
| 20 | Tỉnh Lạng Sơn |
| 22 | Tỉnh Quảng Ninh |
| 24 | Tỉnh Bắc Ninh |
| 25 | Tỉnh Phú Thọ |
| 31 | Thành phố Hải Phòng |
| 33 | Tỉnh Hưng Yên |
| 37 | Tỉnh Ninh Bình |
| 38 | Tỉnh Thanh Hóa |
| 40 | Tỉnh Nghệ An |
| 42 | Tỉnh Hà Tĩnh |
| 44 | Tỉnh Quảng Trị |
| 46 | Thành phố Huế |
| 48 | Thành phố Đà Nẵng |
| 51 | Tỉnh Quảng Ngãi |
| 52 | Tỉnh Gia Lai |
| 56 | Tỉnh Khánh Hòa |
| 66 | Tỉnh Đắk Lắk |
| 68 | Tỉnh Lâm Đồng |
| 75 | Tỉnh Đồng Nai |
| 79 | Thành phố Hồ Chí Minh |
| 80 | Tỉnh Tây Ninh |
| 82 | Tỉnh Đồng Tháp |
| 86 | Tỉnh Vĩnh Long |
| 91 | Tỉnh An Giang |
| 92 | Thành phố Cần Thơ |
| 96 | Tỉnh Cà Mau |
8. Cách sử dụng
Python API
from src import convert_address, batch_convert
# Chuyển đổi 1 địa chỉ
result = convert_address("Phường Phúc Xá, Quận Ba Đình, Thành phố Hà Nội")
print(result.converted) # "Phường Hồng Hà, Thành phố Hà Nội"
print(result.status) # ConversionStatus.SUCCESS
print(result.mapping_type) # MappingType.MERGED
# Hỗ trợ viết tắt
result = convert_address("P. Phúc Xá, Q. Ba Đình, TP. Hà Nội")
print(result.converted) # "Phường Hồng Hà, Thành phố Hà Nội"
# Batch
results = batch_convert(["addr1", "addr2", "addr3"])
CLI
# Cài đặt
uv sync
# Chuyển đổi 1 địa chỉ
uv run address-convert convert "P. Phúc Xá, Q. Ba Đình, TP Hà Nội"
# Chuyển đổi CSV
uv run address-convert batch input.csv output.csv --column address
Cập nhật mapping data
uv run python scripts/build_mapping.py
9. Hạn chế và hướng phát triển
Hạn chế hiện tại:
- Xã bị chia tách (
divided): chỉ chọn ward mặc định (is_default), không hỗ trợ geocoding để chọn chính xác - Parser dựa vào vị trí (right-to-left), có thể sai với địa chỉ không chuẩn format
- Chưa hỗ trợ input không dấu hoàn toàn (ví dụ: "Phuong Phuc Xa")
Hướng phát triển:
- Thêm geocoding cho divided wards (dựa trên tọa độ đường phố)
- Hỗ trợ input không dấu bằng fuzzy matching trên normalized keys
- Thêm REST API endpoint
- Tích hợp với pandas DataFrame cho batch processing lớn