File size: 13,585 Bytes
5a0d16f efd7cfc |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 |
---
license: apache-2.0
---
# 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ỉnh
- `DICT_PROVINCE_WARD_NO_DIVIDED`: `{new_prov: {new_ward: [old_compound_key, ...]}}` — xã không bị chia tách
- `DICT_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:
```json
{
"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 nhau
- `renamed`: Chỉ có 1 compound key cũ trỏ vào ward mới VÀ tên khác nhau
- `merged`: Nhiều compound key cũ cùng trỏ vào 1 ward mới
- `divided`: Từ `DICT_PROVINCE_WARD_DIVIDED`, có `is_default` flag cho mỗi option
### 5. Modules
#### 5.1 `src/models.py` — Data Models
- `MappingType(str, Enum)`: `UNCHANGED`, `RENAMED`, `MERGED`, `DIVIDED`
- `ConversionStatus(str, Enum)`: `SUCCESS`, `PARTIAL`, `NOT_FOUND`
- `AdminUnit(@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.json` lần đầu khi gọi `convert_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`:
```bash
# 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
```python
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
```bash
# 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
```bash
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
|