Skip to content

Instantly share code, notes, and snippets.

@typebrook
Last active April 27, 2025 10:10
Show Gist options
  • Save typebrook/0009131ac914e16573fb93516352c236 to your computer and use it in GitHub Desktop.
Save typebrook/0009131ac914e16573fb93516352c236 to your computer and use it in GitHub Desktop.
Nomalized dataset of Taiwan Address for Great-Recall
dataset/*
!dataset/*md5
address/
*.pmtiles

QuickStart

# download all csv files
make csv

# generate directory "address/" to LV.5 administrative lv5 (鄰)
make addr

# generate the trailing address after LV.5 administrative
make addr.新北市 addr.臺北市 ...

TODO

  • 新北市區里代碼不一致
  • 新竹縣和雲林縣帶有 double quote
  • 新竹縣的 BIG-5 帶有自訂自元,無法解析
  • 新竹市「鄰」後的地址處理
  • 帶有「?」的地址
BEGIN {
OFS = "/"
}
{
last=$NF # save the last field
NF-- # remove the last field
gsub($1 OFS, "") # remove 1st filed
gsub(/,/, "/") # set separator as /
folder = "address/" county "/" $0
if (folders[folder] == "") {
folders[folder] = "[\n\"" last "\""
printf "%s", NR "\t" folder tw " \r"
system("mkdir -p \"" folder "\"")
} else {
folders[folder] = folders[folder] ",\n\"" last "\""
}
}
END {
print
count = 0
for (folder in folders) {
index_file = folder "/index.json"
print count "\t" index_file
print folders[folder] "\n]" > index_file
close(index_file)
count++
}
}
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>地址測試</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>
<body>
<main style="width: 20em; margin-inline: auto;">
<div class="progessbar" style="position: relative; text-align: center; border: 1px dashed blue; height: 16px; width: 100%;">
<div class="text" style="z-index:1; position: absolute; left: 0; top: 0; width: 100%; line-height: 16px;"></div>
<div class="bar" style="position: absolute; left: 0; top: 0; height: 16px; background-color: green;"></div>
</div>
<div class="msg"></div>
<hr>
<div id="addr" style="height: 70px">
<select id="county">
<option value="">請選擇縣市</option>
<option value="彰化縣">彰化縣</option>
<option value="新北市">新北市</option>
<option value="新竹市">新竹市</option>
<option value="新竹縣">新竹縣</option>
<option value="桃園市">桃園市</option>
<option value="臺中市">臺中市</option>
<option value="臺北市">臺北市</option>
<option value="臺東縣">臺東縣</option>
<option value="苗栗縣">苗栗縣</option>
<option value="金門縣">金門縣</option>
<option value="雲林縣">雲林縣</option>
</select>
</div>
<hr>
<input type="text" style="width: 100%"/>
<hr>
<div id="charset" style="display: inline-block; width: fit-content; margin-right: 1em;">
<input type="button" name="鄰" value="鄰" />
<input type="button" name="路" value="路" />
<input type="button" name="街" value="街" />
<input type="button" name="段" value="段" />
<input type="button" name="巷" value="巷" />
<input type="button" name="弄" value="弄" />
<input type="button" name="號" value="號" />
<input type="button" name="樓" value="樓" />
<input type="button" name="之" value="之" />
</div>
<input type="button" name="finish" value="完成填寫" style="background: cyan;" />
<hr>
<textArea disabled style="min-width: 100%; width: fit-content; height: 300px"></textArea>
</main>
</body>
<script>
const container = document.getElementById('addr')
const address = document.querySelector('input')
const log = document.querySelector('textArea')
// Change Listener for each address selector
const nextLevel = (event) => {
const ele = event.target
Array.from(container.querySelectorAll('select'))
.filter(sibling => sibling.id > ele.id)
.forEach(sibling => container.removeChild(sibling))
if (!ele.value) return
const pathList = Array.from(container.children)
.map(select => select.value)
const apiUrl = `https://4ba.tw/address/${pathList.join('/')}/index.json`
log.textContent += `${apiUrl}\n`
if (pathList[3]) pathList[3] += "鄰"
address.value = pathList.join('')
fetch(apiUrl)
.then(response => {
if (!response.ok) return
return response.json()
})
.then(data => {
if (data && data.length > 0) {
log.textContent += `${data.length} options\n`
const nextSelect = document.createElement('select')
nextSelect.id = `s${container.children.length}`
const defaultOption = document.createElement('option')
defaultOption.value = ''
defaultOption.textContent = '請選擇'
nextSelect.appendChild(defaultOption)
data.forEach(optionValue => {
const option = document.createElement('option')
option.value = optionValue
option.textContent = optionValue
nextSelect.appendChild(option)
})
container.appendChild(nextSelect)
nextSelect.addEventListener('change', nextLevel)
} else {
log.textContent += "END of Address\n"
}
})
}
const county = document.getElementById('county')
county.addEventListener('change', nextLevel)
// Add Chinese char into address bar
const charset = document.getElementById('charset')
Array.from(charset.children).forEach(char => {
char.onclick = () => {
address.value += char.name
}
})
</script>
<script>
// 起始時間(計時器的啟動時間)。
const startTime = new Date().getTime()
// 目標時間(要倒數幾秒)。
const targetSeconds = 30
// 初始化。
init(targetSeconds)
// timer.
var timer = function (startTime) {
// 當前時間。
var currentTime = new Date().getTime()
// 當前時間 - 起始時間 = 經過時間。(因為不需要毫秒,所以將結果除以1000。)
var diffSec = Math.round((currentTime - startTime) / 1000)
// 目標時間 - 經過時間 = 剩餘時間。
var remainingTime = targetSeconds - diffSec
// update progess.
update(remainingTime)
if (remainingTime <= 0) {
// stop the timer.
clearInterval(timerId)
// 確保最後顯示的時間為00:00
update(0)
// do anything you want to.
document.querySelector(".msg").textContent = "time up!"
}
}
// start the timer.
var timerId = setInterval( function () { timer(startTime); }, 1000)
// 初始化。此處借用update函式來初次設定進度條。
function init(seconds) {
update(seconds)
}
// update progess with the timer.
function update (seconds) {
barRenderer(seconds)
textRenderer(seconds)
}
// refresh the bar.
function barRenderer (seconds) {
const percent = (seconds / targetSeconds) * 100
const text = document.querySelector(".bar")
text.style.width = percent + "%"
}
// refresh the text of the bar.
function textRenderer (seconds) {
var sec = seconds % 60
var min = Math.floor(seconds / 60)
/* 兩種作法都可以 */
//min = min > 9 ? min : "0" + min
//sec = sec > 9 ? sec : "0" + sec
min = min.toString().padStart(2, '0')
sec = sec.toString().padStart(2, '0')
document.querySelector(".text").textContent = min + ":" + sec
}
</script>
# source: https://www.stat.gov.tw/public/Attachment/712693030RPKUP4RX.xlsx
# * For each code, no leading zero
900701 連江縣 南竿鄉
900702 連江縣 北竿鄉
900703 連江縣 莒光鄉
900704 連江縣 東引鄉
902001 金門縣 金城鎮
902002 金門縣 金沙鎮
902003 金門縣 金湖鎮
902004 金門縣 金寧鄉
902005 金門縣 烈嶼鄉
902006 金門縣 烏坵鄉
1000201 宜蘭縣 宜蘭市
1000202 宜蘭縣 羅東鎮
1000203 宜蘭縣 蘇澳鎮
1000204 宜蘭縣 頭城鎮
1000205 宜蘭縣 礁溪鄉
1000206 宜蘭縣 壯圍鄉
1000207 宜蘭縣 員山鄉
1000208 宜蘭縣 冬山鄉
1000209 宜蘭縣 五結鄉
1000210 宜蘭縣 三星鄉
1000211 宜蘭縣 大同鄉
1000212 宜蘭縣 南澳鄉
1000401 新竹縣 竹北市
1000402 新竹縣 竹東鎮
1000403 新竹縣 新埔鎮
1000404 新竹縣 關西鎮
1000405 新竹縣 湖口鄉
1000406 新竹縣 新豐鄉
1000407 新竹縣 芎林鄉
1000408 新竹縣 橫山鄉
1000409 新竹縣 北埔鄉
1000410 新竹縣 寶山鄉
1000411 新竹縣 峨眉鄉
1000412 新竹縣 尖石鄉
1000413 新竹縣 五峰鄉
1000501 苗栗縣 苗栗市
1000502 苗栗縣 苑裡鎮
1000503 苗栗縣 通霄鎮
1000504 苗栗縣 竹南鎮
1000505 苗栗縣 頭份市
1000506 苗栗縣 後龍鎮
1000507 苗栗縣 卓蘭鎮
1000508 苗栗縣 大湖鄉
1000509 苗栗縣 公館鄉
1000510 苗栗縣 銅鑼鄉
1000511 苗栗縣 南庄鄉
1000512 苗栗縣 頭屋鄉
1000513 苗栗縣 三義鄉
1000514 苗栗縣 西湖鄉
1000515 苗栗縣 造橋鄉
1000516 苗栗縣 三灣鄉
1000517 苗栗縣 獅潭鄉
1000518 苗栗縣 泰安鄉
1000701 彰化縣 彰化市
1000702 彰化縣 鹿港鎮
1000703 彰化縣 和美鎮
1000704 彰化縣 線西鄉
1000705 彰化縣 伸港鄉
1000706 彰化縣 福興鄉
1000707 彰化縣 秀水鄉
1000708 彰化縣 花壇鄉
1000709 彰化縣 芬園鄉
1000710 彰化縣 員林市
1000711 彰化縣 溪湖鎮
1000712 彰化縣 田中鎮
1000713 彰化縣 大村鄉
1000714 彰化縣 埔鹽鄉
1000715 彰化縣 埔心鄉
1000716 彰化縣 永靖鄉
1000717 彰化縣 社頭鄉
1000718 彰化縣 二水鄉
1000719 彰化縣 北斗鎮
1000720 彰化縣 二林鎮
1000721 彰化縣 田尾鄉
1000722 彰化縣 埤頭鄉
1000723 彰化縣 芳苑鄉
1000724 彰化縣 大城鄉
1000725 彰化縣 竹塘鄉
1000726 彰化縣 溪州鄉
1000801 南投縣 南投市
1000802 南投縣 埔里鎮
1000803 南投縣 草屯鎮
1000804 南投縣 竹山鎮
1000805 南投縣 集集鎮
1000806 南投縣 名間鄉
1000807 南投縣 鹿谷鄉
1000808 南投縣 中寮鄉
1000809 南投縣 魚池鄉
1000810 南投縣 國姓鄉
1000811 南投縣 水里鄉
1000812 南投縣 信義鄉
1000813 南投縣 仁愛鄉
1000901 雲林縣 斗六市
1000902 雲林縣 斗南鎮
1000903 雲林縣 虎尾鎮
1000904 雲林縣 西螺鎮
1000905 雲林縣 土庫鎮
1000906 雲林縣 北港鎮
1000907 雲林縣 古坑鄉
1000908 雲林縣 大埤鄉
1000909 雲林縣 莿桐鄉
1000910 雲林縣 林內鄉
1000911 雲林縣 二崙鄉
1000912 雲林縣 崙背鄉
1000913 雲林縣 麥寮鄉
1000914 雲林縣 東勢鄉
1000915 雲林縣 褒忠鄉
1000916 雲林縣 臺西鄉
1000917 雲林縣 元長鄉
1000918 雲林縣 四湖鄉
1000919 雲林縣 口湖鄉
1000920 雲林縣 水林鄉
1001001 嘉義縣 太保市
1001002 嘉義縣 朴子市
1001003 嘉義縣 布袋鎮
1001004 嘉義縣 大林鎮
1001005 嘉義縣 民雄鄉
1001006 嘉義縣 溪口鄉
1001007 嘉義縣 新港鄉
1001008 嘉義縣 六腳鄉
1001009 嘉義縣 東石鄉
1001010 嘉義縣 義竹鄉
1001011 嘉義縣 鹿草鄉
1001012 嘉義縣 水上鄉
1001013 嘉義縣 中埔鄉
1001014 嘉義縣 竹崎鄉
1001015 嘉義縣 梅山鄉
1001016 嘉義縣 番路鄉
1001017 嘉義縣 大埔鄉
1001018 嘉義縣 阿里山鄉
1001301 屏東縣 屏東市
1001302 屏東縣 潮州鎮
1001303 屏東縣 東港鎮
1001304 屏東縣 恆春鎮
1001305 屏東縣 萬丹鄉
1001306 屏東縣 長治鄉
1001307 屏東縣 麟洛鄉
1001308 屏東縣 九如鄉
1001309 屏東縣 里港鄉
1001310 屏東縣 鹽埔鄉
1001311 屏東縣 高樹鄉
1001312 屏東縣 萬巒鄉
1001313 屏東縣 內埔鄉
1001314 屏東縣 竹田鄉
1001315 屏東縣 新埤鄉
1001316 屏東縣 枋寮鄉
1001317 屏東縣 新園鄉
1001318 屏東縣 崁頂鄉
1001319 屏東縣 林邊鄉
1001320 屏東縣 南州鄉
1001321 屏東縣 佳冬鄉
1001322 屏東縣 琉球鄉
1001323 屏東縣 車城鄉
1001324 屏東縣 滿州鄉
1001325 屏東縣 枋山鄉
1001326 屏東縣 三地門鄉
1001327 屏東縣 霧臺鄉
1001328 屏東縣 瑪家鄉
1001329 屏東縣 泰武鄉
1001330 屏東縣 來義鄉
1001331 屏東縣 春日鄉
1001332 屏東縣 獅子鄉
1001333 屏東縣 牡丹鄉
1001401 臺東縣 臺東市
1001402 臺東縣 成功鎮
1001403 臺東縣 關山鎮
1001404 臺東縣 卑南鄉
1001405 臺東縣 鹿野鄉
1001406 臺東縣 池上鄉
1001407 臺東縣 東河鄉
1001408 臺東縣 長濱鄉
1001409 臺東縣 太麻里鄉
1001410 臺東縣 大武鄉
1001411 臺東縣 綠島鄉
1001412 臺東縣 海端鄉
1001413 臺東縣 延平鄉
1001414 臺東縣 金峰鄉
1001415 臺東縣 達仁鄉
1001416 臺東縣 蘭嶼鄉
1001501 花蓮縣 花蓮市
1001502 花蓮縣 鳳林鎮
1001503 花蓮縣 玉里鎮
1001504 花蓮縣 新城鄉
1001505 花蓮縣 吉安鄉
1001506 花蓮縣 壽豐鄉
1001507 花蓮縣 光復鄉
1001508 花蓮縣 豐濱鄉
1001509 花蓮縣 瑞穗鄉
1001510 花蓮縣 富里鄉
1001511 花蓮縣 秀林鄉
1001512 花蓮縣 萬榮鄉
1001513 花蓮縣 卓溪鄉
1001601 澎湖縣 馬公市
1001602 澎湖縣 湖西鄉
1001603 澎湖縣 白沙鄉
1001604 澎湖縣 西嶼鄉
1001605 澎湖縣 望安鄉
1001606 澎湖縣 七美鄉
1001701 基隆市 中正區
1001702 基隆市 七堵區
1001703 基隆市 暖暖區
1001704 基隆市 仁愛區
1001705 基隆市 中山區
1001706 基隆市 安樂區
1001707 基隆市 信義區
1001801 新竹市 東區
1001802 新竹市 北區
1001803 新竹市 香山區
1002001 嘉義市 東區
1002002 嘉義市 西區
6300100 臺北市 松山區
6300200 臺北市 信義區
6300300 臺北市 大安區
6300400 臺北市 中山區
6300500 臺北市 中正區
6300600 臺北市 大同區
6300700 臺北市 萬華區
6300800 臺北市 文山區
6300900 臺北市 南港區
6301000 臺北市 內湖區
6301100 臺北市 士林區
6301200 臺北市 北投區
6400100 高雄市 鹽埕區
6400200 高雄市 鼓山區
6400300 高雄市 左營區
6400400 高雄市 楠梓區
6400500 高雄市 三民區
6400600 高雄市 新興區
6400700 高雄市 前金區
6400800 高雄市 苓雅區
6400900 高雄市 前鎮區
6401000 高雄市 旗津區
6401100 高雄市 小港區
6401200 高雄市 鳳山區
6401300 高雄市 林園區
6401400 高雄市 大寮區
6401500 高雄市 大樹區
6401600 高雄市 大社區
6401700 高雄市 仁武區
6401800 高雄市 鳥松區
6401900 高雄市 岡山區
6402000 高雄市 橋頭區
6402100 高雄市 燕巢區
6402200 高雄市 田寮區
6402300 高雄市 阿蓮區
6402400 高雄市 路竹區
6402500 高雄市 湖內區
6402600 高雄市 茄萣區
6402700 高雄市 永安區
6402800 高雄市 彌陀區
6402900 高雄市 梓官區
6403000 高雄市 旗山區
6403100 高雄市 美濃區
6403200 高雄市 六龜區
6403300 高雄市 甲仙區
6403400 高雄市 杉林區
6403500 高雄市 內門區
6403600 高雄市 茂林區
6403700 高雄市 桃源區
6403800 高雄市 那瑪夏區
6500001 新北市 板橋區
6500002 新北市 三重區
6500003 新北市 中和區
6500004 新北市 永和區
6500005 新北市 新莊區
6500006 新北市 新店區
6500007 新北市 樹林區
6500008 新北市 鶯歌區
6500009 新北市 三峽區
6500010 新北市 淡水區
6500011 新北市 汐止區
6500012 新北市 瑞芳區
6500013 新北市 土城區
6500014 新北市 蘆洲區
6500015 新北市 五股區
6500016 新北市 泰山區
6500017 新北市 林口區
6500018 新北市 深坑區
6500019 新北市 石碇區
6500020 新北市 坪林區
6500021 新北市 三芝區
6500022 新北市 石門區
6500023 新北市 八里區
6500024 新北市 平溪區
6500025 新北市 雙溪區
6500026 新北市 貢寮區
6500027 新北市 金山區
6500028 新北市 萬里區
6500029 新北市 烏來區
6600100 臺中市 中區
6600200 臺中市 東區
6600300 臺中市 南區
6600400 臺中市 西區
6600500 臺中市 北區
6600600 臺中市 西屯區
6600700 臺中市 南屯區
6600800 臺中市 北屯區
6600900 臺中市 豐原區
6601000 臺中市 東勢區
6601100 臺中市 大甲區
6601200 臺中市 清水區
6601300 臺中市 沙鹿區
6601400 臺中市 梧棲區
6601500 臺中市 后里區
6601600 臺中市 神岡區
6601700 臺中市 潭子區
6601800 臺中市 大雅區
6601900 臺中市 新社區
6602000 臺中市 石岡區
6602100 臺中市 外埔區
6602200 臺中市 大安區
6602300 臺中市 烏日區
6602400 臺中市 大肚區
6602500 臺中市 龍井區
6602600 臺中市 霧峰區
6602700 臺中市 太平區
6602800 臺中市 大里區
6602900 臺中市 和平區
6700100 臺南市 新營區
6700200 臺南市 鹽水區
6700300 臺南市 白河區
6700400 臺南市 柳營區
6700500 臺南市 後壁區
6700600 臺南市 東山區
6700700 臺南市 麻豆區
6700800 臺南市 下營區
6700900 臺南市 六甲區
6701000 臺南市 官田區
6701100 臺南市 大內區
6701200 臺南市 佳里區
6701300 臺南市 學甲區
6701400 臺南市 西港區
6701500 臺南市 七股區
6701600 臺南市 將軍區
6701700 臺南市 北門區
6701800 臺南市 新化區
6701900 臺南市 善化區
6702000 臺南市 新市區
6702100 臺南市 安定區
6702200 臺南市 山上區
6702300 臺南市 玉井區
6702400 臺南市 楠西區
6702500 臺南市 南化區
6702600 臺南市 左鎮區
6702700 臺南市 仁德區
6702800 臺南市 歸仁區
6702900 臺南市 關廟區
6703000 臺南市 龍崎區
6703100 臺南市 永康區
6703200 臺南市 東區
6703300 臺南市 南區
6703400 臺南市 北區
6703500 臺南市 安南區
6703600 臺南市 安平區
6703700 臺南市 中西區
6800100 桃園市 桃園區
6800200 桃園市 中壢區
6800300 桃園市 大溪區
6800400 桃園市 楊梅區
6800500 桃園市 蘆竹區
6800600 桃園市 大園區
6800700 桃園市 龜山區
6800800 桃園市 八德區
6800900 桃園市 龍潭區
6801000 桃園市 平鎮區
6801100 桃園市 新屋區
6801200 桃園市 觀音區
6801300 桃園市 復興區
#! /bin/bash
# TODO refactor with coproc
set -e
set -o pipefail
[[ -z "$1" || ! "$1" =~ raw ]] && exit 1
file=${1%.raw}; bak=$file.bak
trap '[ -f $bak ] && rm $bak' EXIT
cp $1 $file
# Transform JSON into CSV
if [ $(head -c1 $file) = '[' ]; then
jq -rc '.[]|"\(.["省市縣市代碼"]),\(.["鄉鎮市區代碼"]),\(.["村里"]),\(.["鄰"]),\(.["街路段 "]),\(.["地區"]),\(.["巷"]),\(.["弄"]),\(.["號"])"' $file >$bak && \
{ echo 省市縣市代碼,鄉鎮市區代碼,村里,鄰,街路段,地區,巷,弄,號; cat $bak; } | tee >head >$file
echo Transform $@ from JSON to CSV
fi
# Remove double quote and process user-defined chars
# FUCK YOU 新竹縣民政處!
if [[ $file =~ 新竹縣 ]]; then
<$file awk --csv -vOFS=, '{
$3 = $2=="10004020" ? "@@@" : $3
$6 = $2=="10004070" && $6!="" ? "###" : $6
print
}' | \
iconv -f BIG-5 -t UTF-8 | \
sed 's/@@@/上舘里/;s/###/山猪湖/' >$bak && \
mv $bak $file
echo Fix chaos in $file
fi
# BIG-5 -> UTF-8
if [[ "$(file -i $file)" =~ charset=iso-8859-1 ]]; then
iconv -f BIG-5 -t UTF-8 $file >$bak && \
mv $bak $file
echo Transform $@ from BIT-5 to UTF-8
fi
# Move trailing fields about coordinates
echo Move XY fields to the beginning
<$file awk -F, -vOFS=, '
NR == 1 {
i = 1
while(i <= NF) {
if ($i ~ /x|橫/) {
x = i
y = i + 1
}
i += 1
}
if (!x || !y) {
exit 1
}
}
NR > 1 {
printf("%s", $x " " $y " 0 ")
NF = x - 1
$1 = ""
print
}
NR % 1000 == 0 { printf("%s", "\r" NR) >> "/dev/tty" }
END { print "" >> "/dev/tty" }
' | \
cs2cs +init=epsg:3826 +to +init=epsg:4326 -f %.7f | \
sed -E 's/\t/ /; s/ 0\.?0* //' >$bak
mv $bak $file
echo Move XY fields: finished
# Cut address after LV.5 adminstrative
# FUCK YOU 新竹市民政處!!
echo Procdssing $file
if [[ $file =~ 新竹市 ]]; then
<$file >$bak awk -F, -vOFS=, '
{
addr = $5
match(addr, /[^0-9]+(街|路|大道)([一-九]+段)?/); $5 = substr(addr, RSTART, RLENGTH)
match(addr, /([^0-9,]+)/); $6 = $5 == "" ? substr(addr, RSTART, RLENGTH) : ""
match(addr, /[0-9]+巷/); $7 = substr(addr, RSTART, RLENGTH)
match(addr, /[0-9]+弄/); $8 = substr(addr, RSTART, RLENGTH)
match(addr, /[0-9之]+號/); $9 = substr(addr, RSTART, RLENGTH)
print
}
'
mv $bak $file
echo Cut address chunks in $file
fi
# Replace code with LV3 administrative
list=admin_lv3.list
[ -f "$list" ] || { echo admin_lv3.list not found; exit 1; }
county=$(basename $file); county=${county%.*}
<"$list" sed -En "s/ +/,/g; /^[^#].*$county/p" | \
awk --csv -vOFS=, '
# save code/admin_name as dictionary
NR==FNR {
dict[$1]=$3
print $1 " => " $3 >> "/dev/tty"
next
}
# replace field1 with code, field2 with LV3 admin
{
printf $1 ","
code=$2
if (length(code) > 7) code=substr(code, 0, 7)
printf dict[code]
for (i=3; i<=NF; i++) printf "," $i; print ""
}
FNR % 1000 == 0 { printf("%s", "\r" FNR) >> "/dev/tty" }
END { print "" >> "/dev/tty" }
' - $file >$bak
mv $bak $file
echo Add name of LV3 administrative into each records
彰化縣 https://data.gov.tw/dataset/170727 https://email.chcg.gov.tw/df/yrp4xnrt1by1y7db2h45biormyhh47
新北市 https://data.gov.tw/dataset/168887 https://data.ntpc.gov.tw/api/datasets/d7b568ab-3819-40c8-a6e7-a6b199443101/csv/file
新竹市 https://data.gov.tw/dataset/157547 https://odws.hccg.gov.tw/001/Upload/25/opendataback/9059/800/75999ddb-2e69-4556-8ebd-d684994e901a.csv
新竹縣 https://data.gov.tw/dataset/172380 https://ws.hsinchu.gov.tw/001/Upload/1/opendata/8774/2663/77f60218-9ad8-47ff-bc0a-2899dc20f27c.csv
桃園市 https://data.gov.tw/dataset/157689 https://opendata.tycg.gov.tw/api/dataset/ec47dbd5-9ed8-4c8d-8ce1-ccb63b1b72e6/resource/56e661b1-cb78-481c-a090-406db06566ee/download
臺中市 https://data.gov.tw/dataset/168960 https://drive.usercontent.google.com/download?id=1olLcrzUoBUGRsRokOWIA1PqwT2FpOG41&export=download&authuser=0&confirm=t&uuid=a4c2630a-f83e-4f5d-855d-f39dd365f86d&at=AEz70l4pPsvuiSAlTAJrwWJpBJS9%3A1743414318766
臺北市 https://data.gov.tw/dataset/155472 https://data.taipei/api/dataset/b7c8e724-1e98-45ee-a0bd-f3840623ed97/resource/ce76ca0c-7f94-4935-ab47-1d2a41ca2abb/download
臺東縣 https://data.gov.tw/dataset/165619 https://ttone.taitung.gov.tw/download?id=dhHIYU0Y8nwyyZA%2BM4KOXg%3D%3D
苗栗縣 https://data.gov.tw/dataset/172475 https://webws.miaoli.gov.tw/Download.ashx?u=LzAwMS9VcGxvYWQvb3BlbmRhdGEvMTc0MC9iNWEzMTAzNy0wZDBhLTQ5MzMtOWQ4OC1kZGMxNTk2MTkxMzkuY3N2&n=MTEz5bm05LiL5Y2K5bm05bqm6IuX5qCX57ij6ZaA54mM6bue5L2N57at6K2357O757Wx6ZaA54mM5bqn5qiZ6LOH5paZLmNzdg%3d%3d&icon=.csv
雲林縣 https://data.gov.tw/dataset/166201 https://ws.yunlin.gov.tw/001/Upload/539/opendata/0/1788/4d7e76b0-9099-4dcf-afe8-0511f703d792.csv https://ws.yunlin.gov.tw/001/Upload/539/opendata/0/1788/41577247-88f8-41b2-b0bb-d3889357952e.csv
231c33d526110bcbe2817441a5723b77 dataset/彰化縣.csv 461806
01117e4382b90ba7e71fe453fc5d4d92 dataset/新北市.csv 1951406
8d5f129d4a10a3ec485f4cb1394baf10 dataset/新竹市.csv 210396
ad6b2f7bc346b4d9ad972d164ae8c060 dataset/新竹縣.csv 258967
28b401ecab05ad6aee13bcde4d954a6d dataset/桃園市.csv 1068369
edc70d21836d7f2ee753ac8913d70c8e dataset/臺中市.csv 1255035
32bf60de05b2aff8c1fede1cb0224f41 dataset/臺北市.csv 1146020
b27faece4f08f3505e78d13c17f35789 dataset/臺東縣.csv 93671
d99b8e9eebc1e45607a74ed9a06fc1b1 dataset/苗栗縣.csv 224282
4c215338bb8756a02ad25875a5d007af dataset/金門縣.csv 32005
cca19bc80c354d2c3089376d7694d728 dataset/雲林縣.csv 277587
# CLI Tools used:
# GNU Coreutils https://www.gnu.org/software/coreutils/
# iconv https://www.gnu.org/software/libc
# curl https://curl.se
# jq https://jqlang.org/
# wikibase-cli https://github.com/maxlath/wikibase-cli
SHELL = /bin/bash
.ONESHELL:
# data.gov.tw: 各縣市政府門牌資料集(CSV)
# FIXME 臺中市(dataset/168960)112年度以後使用 Google Drive 來存取資料集...
counties = $(shell cut -f1 dataset.list | tr '\n' ' ')
csv: $(addprefix dataset/, $(addsuffix .csv, $(counties)))
%.csv: dataset.list
dir=$$(dirname $@); mkdir -p $$dir
county=$$(basename $@); county=$${county%.csv}
echo county $$county
grep -E "^$$county" $< | cut -f3- | sed -E 's/\t/\n/' | \
tee /dev/tty | \
xargs -L1 curl -skL | tr -d '"' | dos2unix >[email protected] && ./csv.fix.sh [email protected]
md5:
rm dataset.md5
ls dataset/*csv | while read csv; do
echo $$(md5sum $$csv) $$(wc -l $$csv | cut -d' ' -f1)
done | tee /dev/tty >>dataset.md5
# 檢查罷免縣市 x 已下載資料集
check: recall.list dataset.list
sed -E 's/.*\s([^\s]+[市縣]).*/\1/' $< | \
sort -u | \
while read county; do
code=$$(grep $$county dataset.list | cut -f2)
if [ -f dataset/$$county.csv ]; then
echo "- [x] $$county $$code"
else
echo "- [ ] $$county $$code"
fi
done
# Directory address/ for api
addr: admin_lv3.list
mkdir -p $@
awk -v OFS=/ '{
code = $$1
county = $$2
lv3 = $$3
dataset = "dataset/" county ".csv"
command = "test -f " dataset " && grep " code " dataset/" county ".csv | \
cut -d, -f3-4 | \
sed s@,@/@g | \
sed s@^@address/" county "/" lv3 "/@ | \
sort -u | \
tee /dev/tty | \
xargs mkdir -p || echo no dataset"
#print
system(command)
}' $<
echo Makding index.json for each directory
declare -i count
find address -maxdepth 3 -type d | \
while read dir; do
((count++))
ls "$$dir" | \
grep -v json | \
jq -Rn '[inputs]' >"$$dir/index.json" && echo -en '\r' $$count || echo -e '\n' "$$dir" fails
done
# 生成五級行政區之下的地址
# 1. 無固定格式,街路段/地區/巷/弄/號 等不一定有值
# 2. 若目錄還不到「號」欄位,以變數 SIZE 決定是否在目錄中製作 index.json
ARRAY_SIZE=$(shell echo $${SIZE:-20})
address.misc: $(addprefix addr., $(counties))
addr.%:
county=$@; county=$${county##*.}
dataset=dataset/$$county.csv; [ -f $$dataset ] || { echo $$dataset not found; exit 1; }
echo dataset $$dataset
<$$dataset sed -E 's/(null|\s)//g; s/,+/,/g; /\?/d' | \
awk -F, -v county=$$county -f ./addr.awk $<
find address/$$county -type d | \
while read dir; do
index=$${dir}/index.json
test -f $$index && continue
ls $$dir | \
grep -v json | \
jq -Rn '[inputs]' >$$index || echo $$dir fails
done
# Get tippecanoe from https://github.com/felt/tippecanoe
.PHONY: output.pmtiles
output.pmtiles: dataset
input=$$(mktemp --suffix .csv)
{
echo X,Y,lv3,lv4,lv5,road,area,xan,ron,num;
cat $</*csv | sed 's/ /,/';
} >$${input}
tippecanoe --force -Z8 -z13 --drop-densest-as-needed --extend-zooms-if-still-dropping --output=$@ $${input}
rm $${input}
彰化縣.csv 下?里
彰化縣.csv 南?村
彰化縣.csv 南?里
彰化縣.csv 埤?村
彰化縣.csv 大?里
彰化縣.csv ?子村
彰化縣.csv 寶?里
彰化縣.csv 新?村
彰化縣.csv 瓦?村
彰化縣.csv 磚?里
彰化縣.csv 舊?村
彰化縣.csv 頂?村
新竹縣.csv 水?村
臺東縣.csv 公?村
臺東縣.csv 台?村
臺東縣.csv 土?村
臺東縣.csv 里?里
雲林縣.csv 崙?里
林沛祥 基隆市選舉區 Q17023592
王鴻薇 臺北市第三選舉區 Q16926780
李彥秀 臺北市第四選舉區 Q15930080
羅智強 臺北市第六選舉區 Q16926792
徐巧芯 臺北市第七選舉區 Q16926776
賴士葆 臺北市第八選舉區 Q16926788
洪孟楷 新北市第一選舉區 Q16928962
葉元之 新北市第七選舉區 Q16928964
張智倫 新北市第八選舉區 Q17025378
林德福 新北市第九選舉區 Q16928967
羅明才 新北市第十一選舉區 Q16928977
廖先翔 新北市第十二選舉區 Q16928978
牛煦庭 桃園市第一選舉區 Q11112195
涂權吉 桃園市第二選舉區 Q11112198
魯明哲 桃園市第三選舉區 Q11112196
萬美玲 桃園市第四選舉區 Q11112209
呂玉玲 桃園市第五選舉區 Q11112202
邱若華 桃園市第六選舉區 Q11112204
鄭正鈐 新竹市選舉區 Q11084018
徐欣瑩 新竹縣第一選舉區 Q11084066
林思銘 新竹縣第二選舉區 Q60684422
邱鎮軍 苗栗縣第二選舉區 Q15921474
顏寬恆 臺中市第二選舉區 Q15886544
楊瓊瓔 臺中市第三選舉區 Q15886553
廖偉翔 臺中市第四選舉區 Q15886139
黃健豪 臺中市第五選舉區 Q16929241
羅廷瑋 臺中市第六選舉區 Q16929243
江啟臣 臺中市第八選舉區 Q16929242
謝衣鳳 彰化縣第三選舉區 Q17025180
馬文君 南投縣第一選舉區 Q19432862
游顥 南投縣第二選舉區 Q10907926
丁學忠 雲林縣第一選舉區 Q49985807
傅崐萁 花蓮縣選舉區 Q17029209
黃建賓 臺東縣選舉區 Q17028006
陳雪生 連江縣選舉區 Q19432864
@typebrook
Copy link
Author

typebrook commented Apr 12, 2025

TO ALL

  1. 這份腳本使用 Makefile,來正規化罷免選區所在的11個縣市政府的門牌資料
    目前共有 10750個村里 698萬筆門牌
    (尚有南投縣連江縣花蓮縣基隆市未取得資料集,已要求 puma 委員索取,FYI @audreyt )

  2. 正規化後的資料可使用指令 make addr address.misc 窮舉出所有地址,製作多級目錄和用於描述當前目錄的index.json
    將之作為伺服器的靜態資源即可完成門牌查詢API

    ./address
    ├── index.json
    ├── 彰化縣
    │   ├── index.json
    │   ├── 二林鎮
    │   │   ├── index.json
    │   │   ├── 中西里
    │   │   │   ├── 1
    │   │   │   │   ├── index.json
    │   │   │   │   └── 舊二路
    │   │   │   │       ├── 395巷
    │   │   │   │       │   ├── 18弄
    │   │   │   │       │   │   └── index.json
    
  3. 目前成果由我的 VPS 提供API服務: https://4ba.tw/address/
    亦可使用指令 wget --recursive https://4ba.tw/address 來鏡像所有資料

  4. 我製作了一個示範頁面,用於展示如何將 API 用於快速登打連署人地址

@typebrook
Copy link
Author

typebrook commented Apr 12, 2025

TO @CarolHsu @muan @imtaiwanese18741130 @e841018 @leeym @Zola

在目前的連署工具中,我對於要求連署人填寫地址的方式並不滿意,包括但不限於:

目前登打的方式,全都是直接使用鍵盤來完成戶籍地址4級行政區以後的部分,
根據志工們的回饋,這樣的方式不論是在連署站用電腦進行登打,或是想要獨立列印的連署人都不甚方便

你們其中一些人曾討論過地址正規化的方案(見 recall-2025 的 #4#7,我也曾在 #10發表過意見)
我認為是完全搞錯方向了

查驗連署書地址是否有效的公務員,其依據必定是地方縣市政府民政處的門牌資料
而既然門牌資料是公開資料集,只要將之化為API給填表頁面使用即可

除了速度快和涵蓋範圍廣之外,這麼做的好處還有幾個:

  1. 若真的有地址被挑毛病,大可以說地址是根據地方政府的公開資料完成的
  2. API 真實重現了身份證上的小寫數字(0-9)和大寫數字(0-9),比較容易說服排斥電子化的罷團使用
    因為手寫是不太可能區分出兩者的
  3. 靜態資源可以隨意部屬,若對於我的API服務不放心,大家也可以直接拿走去用

這邊是我製作的示範頁面,目標是讓95%的登打作業在30秒內完成
你們可以實際邊看身份證邊登打試試,應該會有許多衍生的想法

@Zola
Copy link

Zola commented Apr 13, 2025

由於我是主張離線版,我覺得地址除非是為了較驗用途,通常沒有必要使用API來完成。我見過黑貓宅急便、新竹貨運,都是語音輸入地址比較快捷。

@typebrook
Copy link
Author

typebrook commented Apr 13, 2025

@Zola
你說的大致沒錯,但還是太死腦筋了

通常沒有必要使用API來完成

既然我這整包都可以下載或重新製作,那用瀏覽器直接讀取本機檔案根本不在話下

語音輸入地址比較快

不衝突,這個資料集本來就是補完/驗證一體化,要怎麼用視前端需求而定
所以策略也可以語音輸入+資料集糾正錯字

話說回來,交代給你的作業做完了嗎?

@littlebtc
Copy link

littlebtc commented Apr 14, 2025

https://zh.wikipedia.org/zh-tw/%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E7%AB%8B%E6%B3%95%E5%A7%94%E5%93%A1%E9%81%B8%E8%88%89%E5%8D%80%E5%88%97%E8%A1%A8

(更正) 選舉區有詳細到"里"的選舉區,打星號表示現任立法委員為中國國民黨籍
台北市第一、第二、第三*、第五、第七*、第八*(士林區/中正區*/松山區*)
新北市第二、第三、第四、第五、第六、第七*、第八*、第九*(三重區/新莊區/板橋區*/中和區*)
桃園市第一*、第三*、第四*、第六*(桃園區*/中壢區*)
新竹縣第一*、第二*(竹北市*)
台南市第五、第六(東區)
高雄市第五、第六(苓雅區)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment