UNPKG

hk-address-parser-lib

Version:

The library for the HKAddressParser project

1,646 lines (1,440 loc) 694 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@turf/turf'), require('proj4')) : typeof define === 'function' && define.amd ? define(['exports', '@turf/turf', 'proj4'], factory) : (global = global || self, factory(global['hk-address-parser'] = {}, global.turf, global.proj4)); }(this, function (exports, turf, proj4) { 'use strict'; proj4 = proj4 && proj4.hasOwnProperty('default') ? proj4['default'] : proj4; const region = { HK: { eng: "Hong Kong", chi: "香港" }, KLN: { eng: "Kowloon", chi: "九龍" }, NT: { eng: "New Territories", chi: "新界" } }; const dcDistrict = { invalid: { eng: "Invalid District Name", chi: "無效地區" }, CW: { eng: "Central and Western District", chi: "中西區" }, EST: { eng: "Eastern District", chi: "東區" }, ILD: { eng: "Islands District", chi: "離島區" }, KLC: { eng: "Kowloon City District", chi: "九龍城區" }, KC: { eng: "Kwai Tsing District", chi: "葵青區" }, KT: { eng: "Kwun Tong District", chi: "觀塘區" }, NTH: { eng: "North District", chi: "北區" }, SK: { eng: "Sai Kung District", chi: "西貢區" }, ST: { eng: "Sha Tin Distric", chi: "沙田區" }, SSP: { eng: "Sham Shui Po District", chi: "深水埗區" }, STH: { eng: "Southern District", chi: "南區" }, TP: { eng: "Tai Po District", chi: "大埔區" }, TW: { eng: "Tsuen Wan District", chi: "荃灣區" }, TM: { eng: "Tuen Mun District", chi: "屯門區" }, WC: { eng: "Wan Chai District", chi: "灣仔區" }, WTS: { eng: "Wong Tai Sin District", chi: "黃大仙區" }, YTM: { eng: "Yau Tsim Mong District", chi: "油尖旺區" }, YL: { eng: "Yuen Long District", chi: "元朗區" } }; // MUST use common js style to let address-parse "require" work normally var constants = { region, dcDistrict }; var constants_1 = constants.region; var constants_2 = constants.dcDistrict; /** * To parse the address from OGCIO. this file should be compatible with node.js and js running in browser * */ const CONFIDENT_ALL_MATCH = 1.0; const CONFIDENT_MULTIPLIER_NAME_ONLY = 0.5; const CONFIDENT_MULTIPLIER_PARTIAL_MATCH = 0.7; const CONFIDENT_MULTIPLIER_OPPOSITE_STREET = 0.75; const CONFIDENT_MULTIPLIER_FULL_STREET_MATCH = 1.5; const OGCIO_KEY_BLOCK = "Block"; const OGCIO_KEY_PHASE = "Phase"; const OGCIO_KEY_ESTATE = "Estate"; const OGCIO_KEY_VILLAGE = "Village"; const OGCIO_KEY_STREET = "Street"; const OGCIO_KEY_REGION = "Region"; const OGCIO_KEY_BUILDING_NAME = "BuildingName"; const SCORE_SCHEME = { [OGCIO_KEY_BUILDING_NAME]: 50, [OGCIO_KEY_VILLAGE]: 40, [OGCIO_KEY_ESTATE]: 40, [OGCIO_KEY_STREET]: 40, [OGCIO_KEY_REGION]: 20, [OGCIO_KEY_PHASE]: 20, [OGCIO_KEY_BLOCK]: 20, }; // priority in asscending order const elementPriority = [ OGCIO_KEY_BUILDING_NAME, OGCIO_KEY_BLOCK, OGCIO_KEY_PHASE, OGCIO_KEY_ESTATE, OGCIO_KEY_VILLAGE, OGCIO_KEY_STREET, OGCIO_KEY_REGION ]; const log = console.log; // eslint-disable-line class Match { constructor(confident, matchedKey, matchedWords) { this.confident = confident; this.matchedKey = matchedKey; // array of words that matched this.matchedWords = matchedWords; } } function removeFloor(address) { return address.replace(/([0-9A-z\-\s]+[樓層]|[0-9A-z號\-\s]+[舖鋪]|地[下庫]|平台).*/g, ''); } function dcDistrictMapping(val, isChinese) { for (const district in constants_2) { if (district === val) { return isChinese ? constants_2[district].chi : constants_2[district].eng; } } return isChinese ? constants_2.invalid.chi : constants_2.invalid.eng; } function regionMapping(val) { for (const reg in constants_1) { if (reg === val) { return constants_1[reg].eng; } } } /** * Return the percentage of how much do the laterString match the first one * @param {*} string * @param {*} stringToSearch */ function findPartialMatch(string, stringToSearch) { const match = { matchPercentage: 0, matchedWord: null }; // some exceptional case if the word from OGCIO contains directly the search address, we consider it as a full match if (stringToSearch.indexOf(string) >= 0) { match.matchPercentage = 0.9; match.matchedWord = string; } else { masterLoop: for (let i = 0; i < stringToSearch.length; i ++) { for (let end = stringToSearch.length; end > i; end --) { const substring = stringToSearch.substring(i, end); if (string.includes(substring)) { match.matchPercentage = (substring.length * 1.0 / stringToSearch.length); match.matchedWord = substring; break masterLoop; } } } } return match; } /** * Remove the top level "Eng"/"Chi" prefix of the addresses * @param {*} data */ function eliminateLangKeys(data) { const result = {}; for (const key of Object.keys(data)) { const refinedKey = key.replace(/(^Chi|^Eng)/,''); // eliminate with recursion if (typeof(data[key]) === "object") { result[refinedKey] = eliminateLangKeys(data[key]); } else { result[refinedKey] = data[key]; } } return result; } function normalizeResponse(responseFromOGCIO) { // No more flatten json to maintain the orginal data structure // https://www.als.ogcio.gov.hk/docs/Data_Dictionary_for_ALS_EN.pdf return responseFromOGCIO.SuggestedAddress.map(record => ({ chi: eliminateLangKeys(record.Address.PremisesAddress.ChiPremisesAddress), eng: eliminateLangKeys(record.Address.PremisesAddress.EngPremisesAddress), geo: record.Address.PremisesAddress.GeospatialInformation, })); } function tryToMatchAnyNumber(address, number) { const matches = address.match(/\d+/g); if (matches === null) { return false; } for (const match of matches) { const num = parseInt(match, 10); if (num === number) { return true; } } return false; } function tryToMatchRangeOfNumber(address, from, to, isOdd) { const matches = address.match(/\d+/g); if (matches === null) { return false; } for (const match of matches) { const num = parseInt(match, 10); if (num >= from && num <= to && ((num % 2 === 1) === isOdd)) { return true; } } return false; } function isChinese(s) { return /[^\u0000-\u00ff]/.test(s); } function splitValueForSpaceIfChinese(value) { if (isChinese(value) && /\s/.test(value)) { const tokens = value.split(/\s/); // we need the last element only return tokens[tokens.length - 1]; } return value; } function matchAllMatchedWords(address, matchedWords) { return matchedWords.map(word => address.includes(word)).reduce((p,c) => p && c, true); } /** * Find the longest set of matches that has highest score and not overlapping * @param {*} address * @param {*} matches */ function findMaximumNonOverlappingMatches(address, matches) { if (matches.length === 1) { if (matches[0].matchedWord !== null && matchAllMatchedWords(address, matches[0].matchedWords)) { return matches; } return []; } let longestMatchScore = 0; let longestMatch = []; for (const match of matches) { if (matchAllMatchedWords(address, match.matchedWords)) { let subAddress = address; match.matchedWords.forEach(word => subAddress = subAddress.replace(word, '')); const localLongestMatch = findMaximumNonOverlappingMatches(subAddress, matches.filter(m => m.matchedKey !== match.matchedKey)); localLongestMatch.push(match); const score = calculateScoreFromMatches(localLongestMatch); if (score > longestMatchScore) { longestMatchScore = score; longestMatch = localLongestMatch; } } } return longestMatch; } /** * To calcutate the final confident with partical match * @param {*} confident * @param {*} matchPercentage */ function modifyConfidentByPartialMatchPercentage(confident, matchPercentage) { return confident * matchPercentage * matchPercentage * CONFIDENT_MULTIPLIER_PARTIAL_MATCH; } function calculateScoreFromMatches(matches) { let score = 0; for (const match of matches) { score += SCORE_SCHEME[match.matchedKey] * match.confident; } return score; } function searchSimilarityForStreetOrVillage(type, address, addressToSearch, BuildingNoFrom, BuildingNoTo) { const sim = new Match(0, type, []); if (address.includes(addressToSearch)) { sim.confident = CONFIDENT_ALL_MATCH; sim.matchedWords.push(addressToSearch); } else { const { matchPercentage, matchedWord } = findPartialMatch(address, addressToSearch); if (matchPercentage > 0) { sim.confident = modifyConfidentByPartialMatchPercentage(CONFIDENT_ALL_MATCH, matchPercentage); sim.matchedWords.push(matchedWord); } } // total match of the streetname if (BuildingNoFrom) { const from = parseInt(BuildingNoFrom, 10); const to = BuildingNoTo ? parseInt(BuildingNoTo, 10) : from; const isOdd = parseInt(BuildingNoFrom, 10) % 2 === 1; // If the street name and also the street no. is matched. we should give it a very high score if (from === to) { if (!tryToMatchAnyNumber(address, from)) { if (tryToMatchRangeOfNumber(address, from, to, !isOdd)) { // ratio 1 sim.confident *= CONFIDENT_MULTIPLIER_OPPOSITE_STREET; } else { sim.confident *= CONFIDENT_MULTIPLIER_NAME_ONLY; } } else { sim.matchedWords.push(from + ''); sim.confident *= CONFIDENT_MULTIPLIER_FULL_STREET_MATCH; } } else { if (!tryToMatchRangeOfNumber(address, from, to, isOdd)) { // Try to look up at opposite street if (tryToMatchRangeOfNumber(address, from, to, !isOdd)) { // ratio 1 sim.confident *= CONFIDENT_MULTIPLIER_OPPOSITE_STREET; } else { sim.confident *= CONFIDENT_MULTIPLIER_NAME_ONLY; } } else { // TODO: cannot mark the street/village number that we have came across sim.confident *= CONFIDENT_MULTIPLIER_FULL_STREET_MATCH; } } } else { sim.confident *= CONFIDENT_MULTIPLIER_NAME_ONLY; } return sim; } function searchOccuranceForBlock(address, { BlockDescriptor, BlockNo}) { if (address.includes(BlockNo + BlockDescriptor)) { const match = new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_BLOCK, [BlockNo, BlockDescriptor]); if (BlockNo) { if (!tryToMatchAnyNumber(address, parseInt(BlockNo, 10))) { match.confident = CONFIDENT_MULTIPLIER_NAME_ONLY; } } return match; } return null; } function searchOccuranceForPhase(address, { PhaseNo, PhaseName}) { if (address.includes(PhaseName + PhaseNo)) { const match = new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_PHASE, [PhaseNo, PhaseName]); if (PhaseNo) { if (!tryToMatchAnyNumber(address, parseInt(PhaseNo, 10))) { match.confident = CONFIDENT_MULTIPLIER_NAME_ONLY; } } return match; } return null; } function searchOccuranceForEstate(address, { EstateName }) { if (address.includes(EstateName)) { return new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_ESTATE, [EstateName]); } return null; } function searchOccuranceForRegion(address, region) { if (address.includes(region)) { return new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_REGION, [region]); } return null; } function searchOccuranceForBuildingName(address, buildingName) { if (address.includes(buildingName)) { return new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_BUILDING_NAME, [buildingName]); } else { const { matchPercentage, matchedWord } = findPartialMatch(address, buildingName); if (matchPercentage > 0) { const match = new Match(CONFIDENT_ALL_MATCH, OGCIO_KEY_BUILDING_NAME, [matchedWord]); match.confident = modifyConfidentByPartialMatchPercentage(match.confident, matchPercentage); return match; } } return null; } function searchOccuranceForStreet(address, {StreetName, BuildingNoFrom, BuildingNoTo}) { const streetsToTest = splitValueForSpaceIfChinese(StreetName); return searchSimilarityForStreetOrVillage(OGCIO_KEY_STREET, address, streetsToTest, BuildingNoFrom, BuildingNoTo); } function searchOccuranceForVillage(address, {VillageName, BuildingNoFrom, BuildingNoTo}) { const streetsToTest = splitValueForSpaceIfChinese(VillageName); return searchSimilarityForStreetOrVillage(OGCIO_KEY_VILLAGE, address, streetsToTest, BuildingNoFrom, BuildingNoTo); } /** * Take a */ function searchOccurance(address, ogcioRecordElementKey, ogcioRecordElement) { switch (ogcioRecordElementKey) { case OGCIO_KEY_STREET: return searchOccuranceForStreet(address, ogcioRecordElement); break; case OGCIO_KEY_VILLAGE: return searchOccuranceForVillage(address, ogcioRecordElement); break; case OGCIO_KEY_BLOCK: return searchOccuranceForBlock(address, ogcioRecordElement); break; case OGCIO_KEY_PHASE: return searchOccuranceForPhase(address, ogcioRecordElement); break; case OGCIO_KEY_ESTATE: return searchOccuranceForEstate(address, ogcioRecordElement); break; case OGCIO_KEY_REGION: return searchOccuranceForRegion(address, ogcioRecordElement); break; case OGCIO_KEY_BUILDING_NAME: return searchOccuranceForBuildingName(address, ogcioRecordElement); break; } return null; } function findMatchFromOGCIORecord(address, ogcioRecord) { const matches = []; // First we look up everything that exists in that address for (const key of elementPriority) { if (ogcioRecord.chi[key] !== undefined && isChinese(address)) { // const occurance = searchOccurance(address, key, ogcioRecord.chi[key]); if (occurance === null) { continue; } matches.push(occurance); } if (ogcioRecord.eng[key] !== undefined && !isChinese(address)) { const occurance = searchOccurance(address, key, ogcioRecord.eng[key]); if (occurance === null) { continue; } matches.push(occurance); } } return findMaximumNonOverlappingMatches(address, matches); } function transformDistrict(ogcioRecord) { if (ogcioRecord.eng.District) { ogcioRecord.eng.District.DcDistrict = dcDistrictMapping(ogcioRecord.eng.District.DcDistrict, false); } if (ogcioRecord.chi.District) { ogcioRecord.chi.District.DcDistrict = dcDistrictMapping(ogcioRecord.chi.District.DcDistrict, true); } if (ogcioRecord.eng.Region) { ogcioRecord.eng.Region = regionMapping(ogcioRecord.eng.Region); } return ogcioRecord; } function parseAddress(address, normalizedOGCIOResult) { for (let record of normalizedOGCIOResult) { const matches = findMatchFromOGCIORecord(address, record); record.score = calculateScoreFromMatches(matches); record.matches = matches; // Also tranform the district code to name directly record = transformDistrict(record); } normalizedOGCIOResult = normalizedOGCIOResult.sort((a, b) => { return b.score - a.score; }); return (normalizedOGCIOResult.slice(0, 200)); } /** * Standalone version of address parsing. * @param {*} address * @param {*} responseFromOGCIO Raw json response from ogcio */ function searchResult(address, responseFromOGCIO) { const normalizedAddress = removeFloor(address).toUpperCase(); const normalizedOGCIOResult = normalizeResponse(responseFromOGCIO); return parseAddress(normalizedAddress, normalizedOGCIOResult); } var ogcioParser = { searchResult, calculateScoreFromMatches }; class Address { constructor() { } /** * Return the detailed components of the parsed address * each element should be in form of * { * translatedLabel: * key: * translatedValue: * } */ components(lang) { return []; } componentLabelForKey(key, lang) { const component = this.components(lang).find(component => component.key === key); return component === undefined ? '' : component.translatedLabel; } componentValueForKey(key, lang) { const component = this.components(lang).find(component => component.key === key); return component === undefined ? '' : component.translatedValue; } fullAddress(lang) { return null; } coordinate() { return { lat: 0, lng: 0, } } coordinates() { return []; } // In the future it can be multiple source dataSource() { return null; } /** * Return a normalized confident level from 0 - 10 */ confidence() { return 0; } distanceTo(address) { const cord1 = turf.point([this.coordinate().lng, this.coordinate().lat]); const cord2 = turf.point([address.coordinate().lng, address.coordinate().lat]); return turf.distance(cord1, cord2, {units: 'kilometers'}); } } Address.LANG_EN = 'eng'; Address.LANG_ZH = 'chi'; /** * Some helper functions for showing OGCIO address result * definations: * https://www.als.ogcio.gov.hk/docs/Data_Dictionary_for_ALS_EN.pdf */ const OGCIO_KEY_BLOCK$1 = 'Block'; const OGCIO_KEY_PHASE$1 = 'Phase'; const OGCIO_KEY_ESTATE$1 = 'Estate'; const OGCIO_KEY_VILLAGE$1 = 'Village'; const OGCIO_KEY_REGION$1 = 'Region'; const OGCIO_KEY_STREET$1 = 'Street'; const OGCIO_KEY_DISTRICT = 'District'; const OGCIO_KEY_BUILDING_NAME$1 = 'BuildingName'; const keys = { eng: { // level 1 keys [OGCIO_KEY_BLOCK$1]: 'Block', [OGCIO_KEY_PHASE$1]: 'Phase', [OGCIO_KEY_ESTATE$1]: 'Estate', [OGCIO_KEY_VILLAGE$1]: 'Village', [OGCIO_KEY_REGION$1]: 'Region', [OGCIO_KEY_DISTRICT]: 'District', [OGCIO_KEY_STREET$1]: 'Street', [OGCIO_KEY_BUILDING_NAME$1]: 'Building Name', // level 2 keys [`${OGCIO_KEY_DISTRICT}.DcDistrict`]: '區議會分區', [`${OGCIO_KEY_STREET$1}.StreetName`]: 'Street Name', [`${OGCIO_KEY_STREET$1}.BuildingNoFrom`]: 'Street No. From', [`${OGCIO_KEY_STREET$1}.BuildingNoTo`]: 'Street No. To', [`${OGCIO_KEY_ESTATE$1}.EstateName`]: 'Estate Name', }, chi: { // level 1 keys [OGCIO_KEY_BLOCK$1]: '座數', [OGCIO_KEY_PHASE$1]: '期數', [OGCIO_KEY_ESTATE$1]: '屋邨', [OGCIO_KEY_VILLAGE$1]: '鄉村', [OGCIO_KEY_REGION$1]: '區域', [OGCIO_KEY_DISTRICT]: '地區', [OGCIO_KEY_STREET$1]: '街道', [OGCIO_KEY_BUILDING_NAME$1]: '大廈名稱', // level 2 keys [`${OGCIO_KEY_DISTRICT}.DcDistrict`]: '區議會分區', [`${OGCIO_KEY_STREET$1}.StreetName`]: '街道', [`${OGCIO_KEY_STREET$1}.BuildingNoFrom`]: '街號', [`${OGCIO_KEY_STREET$1}.BuildingNoTo`]: '街號', [`${OGCIO_KEY_ESTATE$1}.EstateName`]: '屋邨', } }; function safeFieldValue(obj, key) { return obj && obj[key] ? obj[key] : ''; } function textForKey(key, lang) { return keys[lang] ? (keys[lang][key] ? keys[lang][key] : key) : key; } function engBuildingNumberFromField(field) { if (!field || (!field.BuildingNoFrom && !field.BuildingNoTo)) { return ''; } if (field.BuildingNoFrom && field.BuildingNoTo) { return `${field.BuildingNoFrom}-${field.BuildingNoTo}`; } else { return `${field.BuildingNoTo ? field.BuildingNoTo : field.BuildingNoFrom}`; } } function chineseBuildingNumberFromField(field) { if (!field || (!field.BuildingNoFrom && !field.BuildingNoTo)) { return ''; } if (field.BuildingNoFrom && field.BuildingNoTo) { return `${field.BuildingNoFrom}至${field.BuildingNoTo}號`; } else { return `${field.BuildingNoTo ? field.BuildingNoTo : field.BuildingNoFrom}號`; } } function prettyPrintBlock(blockObj, lang) { if (lang === 'chi') { return `${blockObj.BlockNo}${blockObj.BlockDescriptor}`; } else if (lang === 'eng') { return `${blockObj.BlockDescriptor} ${blockObj.BlockNo}`; } } function prettyPrintEstate(estateObj, lang) { let estateName = estateObj.EstateName; const phase = estateObj[OGCIO_KEY_PHASE$1]; if (lang === 'chi') { if (phase) { estateName = `${estateName}${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseNo')}${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseName')}`; } } else if (lang === 'eng') { if (phase) { estateName = `${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseName')}${safeFieldValue(estateObj[OGCIO_KEY_PHASE$1], 'PhaseNo')},${estateName}`; } } return estateName; } function prettyPrintStreet(streetObj, lang) { if (lang === 'chi') { return `${safeFieldValue(streetObj, 'StreetName')}${chineseBuildingNumberFromField(streetObj)}`; } else if (lang === 'eng') { return `${engBuildingNumberFromField(streetObj)} ${safeFieldValue(streetObj, 'StreetName')}`; } } function textForValue(record, key, lang) { if (!record[lang]) { return ''; } if (typeof (record[lang][key]) === 'string') { return record[lang][key]; } if (key === OGCIO_KEY_ESTATE$1) { return prettyPrintEstate(record[lang][key], lang); } else if (key === OGCIO_KEY_BLOCK$1) { return prettyPrintBlock(record[lang][key], lang); } else if (key === OGCIO_KEY_STREET$1) { return prettyPrintStreet(record[lang][key], lang); } return Object.values(record[lang][key]).join(); } /** * Format the chinese address from the given result set * @param {*} result */ function fullChineseAddressFromResult(result) { const { Street, Block, Phase, Estate, Village } = result; const region = safeFieldValue(result, 'Region'); const streetName = safeFieldValue(Street, 'StreetName'); const streetNumber = chineseBuildingNumberFromField(Street); const villageName = safeFieldValue(Village, 'VillageName'); const villageNumber = chineseBuildingNumberFromField(Village); const estateName = safeFieldValue(Estate, 'EstateName'); const buildingName = safeFieldValue(result, 'BuildingName'); return `${region}${villageName}${villageNumber}${streetName}${streetNumber}${estateName}${buildingName}`.trim(); } /** * Format the english address from the given result set * @param {*} result */ function fullEnglishAddressFromResult(result) { const { Street, Block, Phase, Estate, Village } = result; const region = safeFieldValue(result, 'Region'); const streetName = safeFieldValue(Street, 'StreetName'); const streetNumber = engBuildingNumberFromField(Street); const villageName = safeFieldValue(Village, 'VillageName'); const villageNumber = engBuildingNumberFromField(Village); const buildingName = safeFieldValue(result, 'BuildingName'); const estateName = safeFieldValue(Estate, 'EstateName'); return [buildingName, estateName, `${streetNumber} ${streetName}`, `${villageNumber} ${villageName}`, region].filter(token => token.match(/\S/)).join(', '); } var ogcioHelper = { topLevelKeys: () => Object.keys(keys.chi).map((key) => ({ key, value: keys.chi[key] })).filter(key => !key.key.includes('.')), textForKey, textForValue, fullEnglishAddressFromResult, fullChineseAddressFromResult }; const toFloat = (value) => { if (typeof value === 'number') { return value; } let val = parseFloat(value); if (isNaN(val)) { return 0; } return val; }; class OGCIOAddress extends Address { constructor(ogcioRecord) { super(); this.record = ogcioRecord; this.flattenedComponents = null; } components(lang) { if (this.flattenedComponents === null) { this.flattenedComponents = this.flattenComponents(); } if (lang === Address.LANG_EN) { return this.flattenedComponents['eng']; } else { return this.flattenedComponents['chi']; } } flattenComponents() { const flattenedComponents = { [Address.LANG_EN]: [], [Address.LANG_ZH]: [], }; const langs = [Address.LANG_ZH, Address.LANG_EN]; for (const lang of langs) { for (const key of Object.keys(this.record[lang])) { flattenedComponents[lang].push({ key, translatedLabel: ogcioHelper.textForKey(key, lang), translatedValue: ogcioHelper.textForValue(this.record, key, lang), }); } } return flattenedComponents; } fullAddress(lang) { if (lang === Address.LANG_EN) { return ogcioHelper.fullEnglishAddressFromResult(this.record['eng']); } else { return ogcioHelper.fullChineseAddressFromResult(this.record['chi']); } } coordinate() { const geo = { lat: 0, lng: 0, }; if (this.record.geo !== undefined && this.record.geo.length > 0) { geo.lat = toFloat(this.record.geo[0].Latitude); geo.lng = toFloat(this.record.geo[0].Longitude); } return geo; } coordinates() { if (this.record.geo !== undefined && this.record.geo.length > 0) { return this.record.geo.map(geo => ({ lat: toFloat(geo.Latitude), lng: toFloat(geo.Longitude) })); } return []; } // In the future it can be multiple source dataSource() { return '資科辦'; } confidence() { return Math.min( 4, (this.record.matches .filter(match => match.matchedKey === key) .map(match => match.confident) .reduce((p, c) => c, 0) * 5) | 0 ); } } class LandAddress extends Address{ constructor(landRecord) { super(); this.record = landRecord; } /** * Return the detailed components of the parsed address * each element should be in form of * { * translatedLabel: * key: * translatedValue: * } */ components(lang) { if (lang === Address.LANG_EN) { return [{ translatedValue: this.record.nameEN, key: 'name', translatedLabel: "Name" }]; } else if (lang === Address.LANG_ZH) { return [{ translatedValue: this.record.nameZH, key: 'name', translatedLabel: "Name" }]; } } fullAddress(lang) { if (lang === Address.LANG_EN) { return this.record.addressEN; } else if (lang === Address.LANG_ZH) { return this.record.addressZH; } } coordinate() { return { lat: toFloat(this.record.lat), lng: toFloat(this.record.lng), } } coordinates() { return [{ lat: toFloat(this.record.lat), lng: toFloat(this.record.lng), }]; } // In the future it can be multiple source dataSource() { return '地政總署'; } /** * Return a normalized confident level from 0 - 10 */ confidence() { return 0; } } const createAddress = function (type, record) { switch (type) { case "ogcio": return new OGCIOAddress(record); case "land" : return new LandAddress(record); default: return new Address(record); } }; proj4.defs([ [ "EPSG:2326", "+proj=tmerc +lat_0=22.31213333333334 +lon_0=114.1785555555556 +k=1 +x_0=836694.05 +y_0=819069.8 +ellps=intl +towgs84=-162.619,-276.959,-161.764,0.067753,-2.24365,-1.15883,-1.09425 +units=m +no_defs"], [ "EPSG:4326", "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs" ] ]); var ProjConvertor = { projTransform: (fromProjection, toProjection, coordinates) => { return proj4(proj4(fromProjection), proj4(toProjection), coordinates) } }; const normalizeString = (str) => { return str.replace(/,/g, ' ') }; const sortLandResult = (searchString, landResults) => { const normalizeSearchString = normalizeString(searchString); const container = landResults.map( landAddress => ({ landAddress, lcs: Math.max( lcs(normalizeSearchString, normalizeString(landAddress.fullAddress('chi'))), lcs(normalizeSearchString, normalizeString(landAddress.fullAddress('eng'))) ), }) ); container.sort( (a, b) => b.lcs - a.lcs); return container.map(c => c.landAddress); }; const lcs = (str1, str2) => { const m = str1.length + 1; const n = str2.length + 1; const lcsTable = new Array(m); for (let i = 0; i < m; i++) { lcsTable[i] = new Array(n); } for (let i = 0; i < m; i++) { for (let j = 0; j < n; j++) { if (i === 0 || j === 0) { lcsTable[i][j] = 0; } else if (str1[i] === str2[j]) { lcsTable[i][j] = 1 + lcsTable[i - 1][j - 1]; } else { lcsTable[i][j] = Math.max(lcsTable[i - 1][j], lcsTable[i][j - 1]); } } } return lcsTable[m - 1][n - 1]; }; var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function unwrapExports (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x.default : x; } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } function getCjsExportFromNamespace (n) { return n && n.default || n; } var domain; // This constructor is used to store event handlers. Instantiating this is // faster than explicitly calling `Object.create(null)` to get a "clean" empty // object (tested with v8 v4.9). function EventHandlers() {} EventHandlers.prototype = Object.create(null); function EventEmitter() { EventEmitter.init.call(this); } // nodejs oddity // require('events') === require('events').EventEmitter EventEmitter.EventEmitter = EventEmitter; EventEmitter.usingDomains = false; EventEmitter.prototype.domain = undefined; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; EventEmitter.init = function() { this.domain = null; if (EventEmitter.usingDomains) { // if there is an active domain, then attach to it. if (domain.active && !(this instanceof domain.Domain)) ; } if (!this._events || this._events === Object.getPrototypeOf(this)._events) { this._events = new EventHandlers(); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || isNaN(n)) throw new TypeError('"n" argument must be a positive number'); this._maxListeners = n; return this; }; function $getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return $getMaxListeners(this); }; // These standalone emit* functions are used to optimize calling of event // handlers for fast cases because emit() itself often has a variable number of // arguments and can be deoptimized because of that. These functions always have // the same number of arguments and thus do not get deoptimized, so the code // inside them can execute faster. function emitNone(handler, isFn, self) { if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } function emitOne(handler, isFn, self, arg1) { if (isFn) handler.call(self, arg1); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1); } } function emitTwo(handler, isFn, self, arg1, arg2) { if (isFn) handler.call(self, arg1, arg2); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2); } } function emitThree(handler, isFn, self, arg1, arg2, arg3) { if (isFn) handler.call(self, arg1, arg2, arg3); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2, arg3); } } function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } EventEmitter.prototype.emit = function emit(type) { var er, handler, len, args, i, events, domain; var doError = (type === 'error'); events = this._events; if (events) doError = (doError && events.error == null); else if (!doError) return false; domain = this.domain; // If there is no 'error' event listener then throw. if (doError) { er = arguments[1]; if (domain) { if (!er) er = new Error('Uncaught, unspecified "error" event'); er.domainEmitter = this; er.domain = domain; er.domainThrown = false; domain.emit('error', er); } else if (er instanceof Error) { throw er; // Unhandled 'error' event } else { // At least give some kind of context to the user var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); err.context = er; throw err; } return false; } handler = events[type]; if (!handler) return false; var isFn = typeof handler === 'function'; len = arguments.length; switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: emitOne(handler, isFn, this, arguments[1]); break; case 3: emitTwo(handler, isFn, this, arguments[1], arguments[2]); break; case 4: emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); break; // slower default: args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; emitMany(handler, isFn, this, args); } return true; }; function _addListener(target, type, listener, prepend) { var m; var events; var existing; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = target._events; if (!events) { events = target._events = new EventHandlers(); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } existing = events[type]; } if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { // If we've already got an array, just append. if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } // Check for listener leak if (!existing.warned) { m = $getMaxListeners(target); if (m && m > 0 && existing.length > m) { existing.warned = true; var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + type + ' listeners added. ' + 'Use emitter.setMaxListeners() to increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; emitWarning(w); } } } return target; } function emitWarning(e) { typeof console.warn === 'function' ? console.warn(e) : console.log(e); } EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); }; function _onceWrap(target, type, listener) { var fired = false; function g() { target.removeListener(type, g); if (!fired) { fired = true; listener.apply(target, arguments); } } g.listener = listener; return g; } EventEmitter.prototype.once = function once(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.prependListener(type, _onceWrap(this, type, listener)); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = this._events; if (!events) return this; list = events[type]; if (!list) return this; if (list === listener || (list.listener && list.listener === listener)) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else { delete events[type]; if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { position = -1; for (i = list.length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { originalListener = list[i].listener; position = i; break; } } if (position < 0) return this; if (list.length === 1) { list[0] = undefined; if (--this._eventsCount === 0) { this._events = new EventHandlers(); return this; } else { delete events[type]; } } else { spliceOne(list, position); } if (events.removeListener) this.emit('removeListener', type, originalListener || listener); } return this; }; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events; events = this._events; if (!events) return this; // not listening for removeListener, no need to emit if (!events.removeListener) { if (arguments.length === 0) { this._events = new EventHandlers(); this._eventsCount = 0; } else if (events[type]) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else delete events[type]; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { var keys = Object.keys(events); for (var i = 0, key; i < keys.length; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = new EventHandlers(); this._eventsCount = 0; return this; } listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners) { // LIFO order do { this.removeListener(type, listeners[listeners.length - 1]); } while (listeners[0]); } return this; }; EventEmitter.prototype.listeners = function listeners(type) { var evlistener; var ret; var events = this._events; if (!events) ret = []; else { evlistener = events[type]; if (!evlistener) ret = []; else if (typeof evlistener === 'function') ret = [evlistener.listener || evlistener]; else ret = unwrapListeners(evlistener); } return ret; }; EventEmitter.listenerCount = function(emitter, type) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(type); } else { return listenerCount.call(emitter, type); } }; EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; if (events) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener) { return evlistener.length; } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; }; // About 1.5x faster than the two-arg version of Array#splice(). function spliceOne(list, index) { for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); } function arrayClone(arr, i) { var copy = new Array(i); while (i--) copy[i] = arr[i]; return copy; } function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { ret[i] = arr[i].listener || arr[i]; } return ret; } // shim for using process in browser // based off https://github.com/defunctzombie/node-process/blob/master/browser.js function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } function defaultClearTimeout () { throw new Error('clearTimeout has not been defined'); } var cachedSetTimeout = defaultSetTimout; var cachedClearTimeout = defaultClearTimeout; if (typeof global.setTimeout === 'function') { cachedSetTimeout = setTimeout; } if (typeof global.clearTimeout === 'function') { cachedClearTimeout = clearTimeout; } function runTimeout(fun) { if (cachedSetTimeout === setTimeout) { //normal enviroments in sane situations return setTimeout(fun, 0); } // if setTimeout wasn't available but was latter defined if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { cachedSetTimeout = setTimeout; return setTimeout(fun, 0); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedSetTimeout(fun, 0); } catch(e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedSetTimeout.call(null, fun, 0); } catch(e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error return cachedSetTimeout.call(this, fun, 0); } } } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { //normal enviroments in sane situations return clearTimeout(marker); } // if clearTimeout wasn't available but was latter defined if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { cachedClearTimeout = clearTimeout; return clearTimeout(marker); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedClearTimeout(marker); } catch (e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedClearTimeout.call(null, marker); } catch (e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. // Some versions of I.E. have different rules for clearTimeout vs setTimeout return cachedClearTimeout.call(this, marker); } } } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { if (currentQueue) { currentQueue[queueIndex].run(); } } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; runClearTimeout(timeout); } function nextTick(fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { runTimeout(drainQueue); } } // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; var title = 'browse