1541 lines
62 KiB
JavaScript
1541 lines
62 KiB
JavaScript
// Address Generator
|
||
|
||
import { randomElement, randomInt, generatePhoneNumber, generateEmail, formatAddress } from './utils.js';
|
||
import { getConfig } from './config.js';
|
||
|
||
function ensureNameArray(maybeList) {
|
||
if (Array.isArray(maybeList)) return maybeList;
|
||
if (!maybeList) return [];
|
||
// Support { male: [...], female: [...] } shape by flattening
|
||
if (typeof maybeList === 'object') {
|
||
const out = [];
|
||
if (Array.isArray(maybeList.male)) out.push(...maybeList.male);
|
||
if (Array.isArray(maybeList.female)) out.push(...maybeList.female);
|
||
return out;
|
||
}
|
||
return [];
|
||
}
|
||
|
||
// Data cache to reduce server requests
|
||
const dataCache = new Map();
|
||
const CACHE_PREFIX = 'address_data_cache_';
|
||
const CACHE_VERSION = 'v1'; // Increment this to invalidate old cache
|
||
const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||
|
||
// Load data from JSON file with caching (memory + localStorage)
|
||
async function loadData(filePath) {
|
||
try {
|
||
// Check memory cache first (fastest)
|
||
if (dataCache.has(filePath)) {
|
||
return dataCache.get(filePath);
|
||
}
|
||
|
||
// Check localStorage cache (survives page refresh)
|
||
const cacheKey = CACHE_PREFIX + filePath;
|
||
try {
|
||
const cachedData = localStorage.getItem(cacheKey);
|
||
if (cachedData) {
|
||
const parsed = JSON.parse(cachedData);
|
||
// Check if cache is still valid (not expired)
|
||
if (parsed.timestamp && (Date.now() - parsed.timestamp) < CACHE_EXPIRY) {
|
||
// Restore to memory cache for faster access
|
||
dataCache.set(filePath, parsed.data);
|
||
return parsed.data;
|
||
} else {
|
||
// Cache expired, remove it
|
||
localStorage.removeItem(cacheKey);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// If localStorage fails (e.g., private mode), continue to fetch
|
||
console.warn('localStorage cache read failed:', e);
|
||
}
|
||
|
||
// Get user configuration
|
||
const config = getConfig();
|
||
const fileName = filePath.split('/').pop();
|
||
|
||
// Build paths array based on configuration
|
||
const paths = [];
|
||
|
||
// If user has configured a custom dataBasePath, use it first
|
||
if (config.dataBasePath) {
|
||
// Ensure trailing slash
|
||
const basePath = config.dataBasePath.endsWith('/') ? config.dataBasePath : config.dataBasePath + '/';
|
||
paths.push(basePath + fileName);
|
||
}
|
||
|
||
// If autoDetectPaths is enabled (default), add automatic path detection
|
||
// This preserves the original behavior for mockaddress.com
|
||
if (config.autoDetectPaths !== false) {
|
||
const currentPath = window.location.pathname;
|
||
|
||
// Try multiple possible paths
|
||
// Priority: relative path (../data/) first to avoid cross-language references
|
||
paths.push(
|
||
`../data/${fileName}`, // Relative: go up one level, then into data (works for all language versions)
|
||
`data/${fileName}`, // Relative to current directory (fallback)
|
||
filePath // Original path (fallback)
|
||
);
|
||
|
||
// Add language-specific absolute paths if we're in a language subdirectory
|
||
const pathParts = currentPath.split('/').filter(p => p && p !== 'index.html' && p !== '');
|
||
if (pathParts.length >= 1 && ['en', 'ru', 'es', 'pt'].includes(pathParts[0])) {
|
||
// We're in a language subdirectory, add language-specific absolute path
|
||
const lang = pathParts[0];
|
||
paths.splice(paths.length - 2, 0, `/${lang}/data/${fileName}`); // Insert before fallback paths
|
||
} else {
|
||
// We're in root (Chinese version), add root absolute path
|
||
paths.splice(paths.length - 2, 0, `/data/${fileName}`); // Insert before fallback paths
|
||
}
|
||
}
|
||
|
||
let lastError = null;
|
||
for (const path of paths) {
|
||
try {
|
||
const response = await fetch(path, {
|
||
// Add cache control to help browser cache
|
||
cache: 'default'
|
||
});
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
// Store in memory cache for current session
|
||
dataCache.set(filePath, data);
|
||
|
||
// Also store in localStorage for persistence across page refreshes
|
||
try {
|
||
const cacheKey = CACHE_PREFIX + filePath;
|
||
const cacheData = {
|
||
data: data,
|
||
timestamp: Date.now(),
|
||
version: CACHE_VERSION
|
||
};
|
||
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
|
||
} catch (e) {
|
||
// If localStorage fails (e.g., quota exceeded, private mode), continue
|
||
console.warn('localStorage cache write failed:', e);
|
||
}
|
||
|
||
return data;
|
||
} else {
|
||
// Store the error but continue trying
|
||
lastError = `HTTP ${response.status} for ${path}`;
|
||
}
|
||
} catch (e) {
|
||
// Store the error but continue trying
|
||
lastError = e.message;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// If all paths failed, log error but still throw to allow error handling
|
||
console.error(`Failed to load ${filePath}. Tried paths:`, paths);
|
||
console.error(`Last error:`, lastError);
|
||
throw new Error(`Failed to load ${filePath} from any path`);
|
||
} catch (error) {
|
||
console.error(`Error loading data from ${filePath}:`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate US address
|
||
export async function generateUSAddress(selectedState = 'RANDOM') {
|
||
try {
|
||
const usData = await loadData('data/usData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Select state
|
||
let stateCode = selectedState;
|
||
if (selectedState === 'RANDOM') {
|
||
const states = Object.keys(usData.states);
|
||
stateCode = randomElement(states);
|
||
}
|
||
|
||
const state = usData.states[stateCode];
|
||
if (!state) {
|
||
throw new Error(`State ${stateCode} not found`);
|
||
}
|
||
|
||
// Generate name - decide gender first, then select name
|
||
const nameGroup = namesData.nameGroups.western;
|
||
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
|
||
// Select name based on gender - use gender-specific name lists if available
|
||
let firstName;
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
// Use gender-specific name lists
|
||
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
|
||
} else {
|
||
// Fallback: use all names if gender classification not available
|
||
firstName = randomElement(nameGroup.first);
|
||
}
|
||
const lastName = randomElement(nameGroup.last);
|
||
|
||
// Generate phone
|
||
const areaCode = randomElement(state.area_codes);
|
||
const phone = generatePhoneNumber(areaCode);
|
||
|
||
// Generate email
|
||
const email = generateEmail(firstName, lastName);
|
||
|
||
// Generate address components - use common US street names (these exist in most US cities)
|
||
const streetNumber = randomInt(100, 9999);
|
||
const streetNames = [
|
||
'Main Street', 'Oak Avenue', 'Park Road', 'Maple Drive', 'Elm Street',
|
||
'Washington Avenue', 'Lincoln Street', 'Jefferson Drive', 'Madison Road',
|
||
'Franklin Avenue', 'Church Street', 'Market Street', 'Broadway',
|
||
'First Street', 'Second Street', 'Third Avenue', 'Fourth Street',
|
||
'Fifth Street', 'Sixth Street', 'Seventh Street', 'Eighth Street',
|
||
'Ninth Street', 'Tenth Street', 'Pine Street', 'Cedar Avenue',
|
||
'Spring Street', 'Summer Street', 'Winter Street', 'Lake Avenue',
|
||
'River Road', 'Hill Street', 'Valley Drive', 'Forest Avenue',
|
||
'Garden Street', 'Rose Avenue', 'Sunset Boulevard', 'Sunrise Drive',
|
||
'College Avenue', 'University Drive', 'School Street', 'Library Lane'
|
||
];
|
||
const streetName = randomElement(streetNames);
|
||
const street = `${streetNumber} ${streetName}`;
|
||
|
||
// Generate city - use state-specific cities if available, otherwise use fallback
|
||
let city;
|
||
if (state.cities && state.cities.length > 0) {
|
||
// Use cities from the state's city list
|
||
city = randomElement(state.cities);
|
||
} else {
|
||
// Fallback: use common US city names (should not happen if data is complete)
|
||
const fallbackCities = [
|
||
'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix',
|
||
'Philadelphia', 'San Antonio', 'San Diego', 'Dallas', 'San Jose',
|
||
'Austin', 'Jacksonville', 'Fort Worth', 'Columbus', 'Charlotte',
|
||
'San Francisco', 'Indianapolis', 'Seattle', 'Denver', 'Washington'
|
||
];
|
||
city = randomElement(fallbackCities);
|
||
}
|
||
|
||
// Generate zip code - use state-specific zip range if available, otherwise use fallback
|
||
let zip;
|
||
if (state.zip_range && state.zip_range.min && state.zip_range.max) {
|
||
// Use zip code range from the state's zip_range
|
||
zip = randomInt(state.zip_range.min, state.zip_range.max).toString();
|
||
} else {
|
||
// Fallback: use random 5-digit zip code (should not happen if data is complete)
|
||
zip = randomInt(10000, 99999).toString();
|
||
}
|
||
|
||
// Full address should be in English only
|
||
const stateNameEn = state.name.en || state.name.zh;
|
||
const fullAddress = `${street}, ${city}, ${stateNameEn} ${zip}`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street,
|
||
city,
|
||
state: stateNameEn, // 显示英文州名
|
||
stateCode,
|
||
zip,
|
||
fullAddress,
|
||
country: 'US'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating US address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate Hong Kong address
|
||
// selectedRegion: 'RANDOM' | 'HK' | 'KL' | 'NT'
|
||
// isEnglish: true -> 生成英文姓名和英文地址;false -> 保持中文
|
||
export async function generateHKAddress(selectedRegion = 'RANDOM', isEnglish = false) {
|
||
try {
|
||
const hkData = await loadData('data/hkData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Filter districts based on selected region
|
||
let availableDistricts = {};
|
||
if (selectedRegion === 'RANDOM') {
|
||
// Use all districts
|
||
availableDistricts = hkData.districts;
|
||
} else if (hkData.districts[selectedRegion]) {
|
||
// Use only the selected region
|
||
availableDistricts[selectedRegion] = hkData.districts[selectedRegion];
|
||
} else {
|
||
// Fallback to all districts if invalid selection
|
||
availableDistricts = hkData.districts;
|
||
}
|
||
|
||
// Select random district and area from filtered districts
|
||
const districts = Object.keys(availableDistricts);
|
||
if (districts.length === 0) {
|
||
throw new Error('No districts available for selected region');
|
||
}
|
||
|
||
const districtKey = randomElement(districts);
|
||
const district = availableDistricts[districtKey];
|
||
const area = randomElement(district.areas);
|
||
|
||
// Generate name
|
||
let firstName;
|
||
let lastName;
|
||
let gender;
|
||
|
||
if (isEnglish && hkData.names && hkData.names.en) {
|
||
// 英文模式:使用 hkData 内置的英文化姓名(香港拼音格式)
|
||
// 为了符合Apple ID等注册要求,名字组合成2-3个字(更符合香港人传统姓名习惯)
|
||
const nameGroupEn = hkData.names.en;
|
||
const isMale = Math.random() > 0.5;
|
||
gender = isMale ? 'Male' : 'Female'; // 修复:性别逻辑正确对应
|
||
const firstPool = isMale ? nameGroupEn.first.male : nameGroupEn.first.female;
|
||
|
||
// 组合2-3个字的名字(70%概率双字,30%概率三字)
|
||
if (firstPool && firstPool.length > 0) {
|
||
const nameCount = Math.random() < 0.7 ? 2 : 3; // 70%双字,30%三字
|
||
const selectedNames = [];
|
||
const availableNames = [...firstPool]; // 复制数组避免修改原数组
|
||
|
||
for (let i = 0; i < nameCount && availableNames.length > 0; i++) {
|
||
const selected = randomElement(availableNames);
|
||
selectedNames.push(selected);
|
||
// 移除已选的名字,避免重复
|
||
const index = availableNames.indexOf(selected);
|
||
if (index > -1) availableNames.splice(index, 1);
|
||
}
|
||
|
||
firstName = selectedNames.join(' '); // 用空格连接,如 "Wing Man" 或 "Wing Man Kai"
|
||
} else {
|
||
firstName = randomElement(firstPool || []);
|
||
}
|
||
|
||
lastName = randomElement(nameGroupEn.last || []);
|
||
|
||
// 如果数据异常,兜底
|
||
if (!firstName || !lastName) {
|
||
const fallback = namesData.nameGroups.western;
|
||
gender = Math.random() > 0.5 ? 'Male' : 'Female';
|
||
firstName = randomElement(
|
||
gender === 'Male' ? fallback.first.male : fallback.first.female
|
||
);
|
||
lastName = randomElement(fallback.last);
|
||
}
|
||
} else {
|
||
// 中文模式:沿用原有中文姓名逻辑
|
||
const nameGroup = namesData.nameGroups.chinese;
|
||
gender = Math.random() > 0.5 ? '男' : '女'; // 中文:男/女
|
||
firstName = randomElement(gender === '男' ? nameGroup.first.male : nameGroup.first.female);
|
||
lastName = randomElement(nameGroup.last);
|
||
}
|
||
|
||
// Generate phone (Hong Kong format: +852 XXXX XXXX)
|
||
const phone = `+852 ${randomInt(2000, 9999)} ${randomInt(1000, 9999)}`;
|
||
|
||
// Hong Kong has no official postal code; use fixed placeholder for forms
|
||
const zip = '000000';
|
||
|
||
// Generate email - use English names for email to avoid Chinese characters
|
||
const englishNameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
|
||
let emailFirstName, emailLastName;
|
||
if (englishNameGroup && englishNameGroup.first && englishNameGroup.last) {
|
||
const firstList = ensureNameArray(englishNameGroup.first);
|
||
const lastList = ensureNameArray(englishNameGroup.last);
|
||
emailFirstName = randomElement(firstList);
|
||
emailLastName = randomElement(lastList);
|
||
} else {
|
||
// Fallback: generate random English username
|
||
const randomNames = ['john', 'mary', 'david', 'sarah', 'michael', 'emily', 'james', 'lisa', 'robert', 'anna'];
|
||
emailFirstName = randomElement(randomNames);
|
||
emailLastName = randomElement(randomNames);
|
||
}
|
||
const email = generateEmail(emailFirstName, emailLastName);
|
||
|
||
// Generate address
|
||
const floor = randomInt(1, 50);
|
||
const unit = randomInt(1, 20);
|
||
|
||
let street;
|
||
let building;
|
||
let address;
|
||
let city;
|
||
let districtName;
|
||
let fullAddress;
|
||
|
||
if (isEnglish) {
|
||
street = randomElement(area.streets.en);
|
||
building = randomElement(area.buildings.en);
|
||
// 英文地址格式:Flat 12, 32/F, Building Name, Street Name
|
||
address = `Flat ${unit}, ${floor}/F, ${building}, ${street}`;
|
||
city = area.name_en;
|
||
districtName = district.name.en;
|
||
fullAddress = `${address}, ${city}, ${districtName}, Hong Kong`;
|
||
// 英文模式下,性别字段统一英文化
|
||
gender = gender === '男' ? 'Male' : gender === '女' ? 'Female' : gender;
|
||
} else {
|
||
street = randomElement(area.streets.zh);
|
||
building = randomElement(area.buildings.zh);
|
||
address = `${street} ${building} ${floor}樓 ${unit}室`;
|
||
city = area.name_zh;
|
||
districtName = district.name.zh;
|
||
fullAddress = `${address}, ${city}, ${districtName}`;
|
||
}
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street: address,
|
||
city, // 区域作为城市(中英文根据模式切换)
|
||
county: districtName, // 区作为区县
|
||
district: districtName, // 保留原字段以兼容
|
||
area: city, // 保留原字段以兼容
|
||
fullAddress,
|
||
zip,
|
||
country: 'HK'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating HK address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate UK address
|
||
export async function generateUKAddress(selectedRegion = 'RANDOM') {
|
||
try {
|
||
const ukData = await loadData('data/ukData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Filter regions based on selected region
|
||
let availableRegions = {};
|
||
if (selectedRegion === 'RANDOM') {
|
||
// Use all regions
|
||
availableRegions = ukData.regions;
|
||
} else if (ukData.regions[selectedRegion]) {
|
||
// Use only the selected region
|
||
availableRegions[selectedRegion] = ukData.regions[selectedRegion];
|
||
} else {
|
||
// Fallback to all regions if invalid selection
|
||
availableRegions = ukData.regions;
|
||
}
|
||
|
||
// Select random region from filtered regions
|
||
const regions = Object.keys(availableRegions);
|
||
if (regions.length === 0) {
|
||
throw new Error('No regions available for selected region');
|
||
}
|
||
|
||
const regionKey = randomElement(regions);
|
||
const region = availableRegions[regionKey];
|
||
|
||
// Generate name - decide gender first, then select name
|
||
const nameGroup = namesData.nameGroups.western;
|
||
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
|
||
// Select name based on gender - use gender-specific name lists if available
|
||
let firstName;
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
// Use gender-specific name lists
|
||
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
|
||
} else {
|
||
// Fallback: use all names if gender classification not available
|
||
firstName = randomElement(nameGroup.first);
|
||
}
|
||
const lastName = randomElement(nameGroup.last);
|
||
|
||
// Generate phone
|
||
const phoneCode = randomElement(region.phone_codes);
|
||
const phone = `0${phoneCode} ${randomInt(1000, 9999)} ${randomInt(100000, 999999)}`;
|
||
|
||
// Generate email
|
||
const email = generateEmail(firstName, lastName);
|
||
|
||
// Generate address
|
||
const streetNumbers = [randomInt(1, 999), randomInt(1, 999)];
|
||
const streetName = randomElement([
|
||
'High Street', 'Church Road', 'Park Avenue', 'Main Road', 'London Road',
|
||
'Victoria Street', 'King Street', 'Queen Street', 'Market Street',
|
||
'Station Road', 'Mill Lane', 'Bridge Street', 'New Street', 'Old Street',
|
||
'Castle Street', 'Church Street', 'School Lane', 'Garden Street', 'Hill Road',
|
||
'Oak Avenue', 'Elm Street', 'Maple Drive', 'Cedar Road', 'Pine Street',
|
||
'Rose Lane', 'Lily Street', 'Orchard Road', 'Meadow Way', 'River Street'
|
||
]);
|
||
const street = `${streetNumbers[0]}${streetNumbers[1] > 0 ? '-' + streetNumbers[1] : ''} ${streetName}`;
|
||
|
||
// Generate city - use region-specific cities if available, otherwise use fallback
|
||
let city;
|
||
if (region.cities && region.cities.length > 0) {
|
||
// Use cities from the region's city list
|
||
city = randomElement(region.cities);
|
||
} else {
|
||
// Fallback: use common UK city names (should not happen if data is complete)
|
||
const fallbackCities = [
|
||
'London', 'Manchester', 'Birmingham', 'Liverpool', 'Leeds',
|
||
'Glasgow', 'Edinburgh', 'Bristol', 'Cardiff', 'Belfast'
|
||
];
|
||
city = randomElement(fallbackCities);
|
||
}
|
||
|
||
// UK postcode format: SW1A 1AA - use region-specific postcode areas if available
|
||
let postcodeArea;
|
||
if (region.postcode_areas && region.postcode_areas.length > 0) {
|
||
// Use postcode areas from the region's list
|
||
postcodeArea = randomElement(region.postcode_areas);
|
||
} else {
|
||
// Fallback: use common UK postcode areas (should not happen if data is complete)
|
||
postcodeArea = randomElement(['SW', 'NW', 'SE', 'NE', 'W', 'E', 'N', 'S']);
|
||
}
|
||
const postcode = `${postcodeArea}${randomInt(1, 9)}${randomElement(['A', 'B', 'C'])} ${randomInt(1, 9)}${randomElement(['A', 'B', 'C'])}${randomElement(['A', 'B', 'C'])}`;
|
||
|
||
// Use English region name for full address and region field
|
||
const regionNameEn = region.name.en || region.name.zh;
|
||
const fullAddress = `${street}, ${city}, ${postcode}, ${regionNameEn}`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street,
|
||
city,
|
||
postcode,
|
||
region: regionNameEn, // 显示英文地区名
|
||
fullAddress,
|
||
country: 'UK'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating UK address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate Canada address
|
||
export async function generateCAAddress(selectedProvince = 'RANDOM') {
|
||
try {
|
||
const caData = await loadData('data/caData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Filter provinces based on selected province
|
||
let availableProvinces = {};
|
||
if (selectedProvince === 'RANDOM') {
|
||
// Use all provinces
|
||
availableProvinces = caData.provinces;
|
||
} else if (caData.provinces[selectedProvince]) {
|
||
// Use only the selected province
|
||
availableProvinces[selectedProvince] = caData.provinces[selectedProvince];
|
||
} else {
|
||
// Fallback to all provinces if invalid selection
|
||
availableProvinces = caData.provinces;
|
||
}
|
||
|
||
// Select random province from filtered provinces
|
||
const provinces = Object.keys(availableProvinces);
|
||
if (provinces.length === 0) {
|
||
throw new Error('No provinces available for selected province');
|
||
}
|
||
|
||
const provinceKey = randomElement(provinces);
|
||
const province = availableProvinces[provinceKey];
|
||
|
||
// Generate name - decide gender first, then select name
|
||
const nameGroup = namesData.nameGroups.western;
|
||
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
|
||
// Select name based on gender - use gender-specific name lists if available
|
||
let firstName;
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
// Use gender-specific name lists
|
||
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
|
||
} else {
|
||
// Fallback: use all names if gender classification not available
|
||
firstName = randomElement(nameGroup.first);
|
||
}
|
||
const lastName = randomElement(nameGroup.last);
|
||
|
||
// Generate phone
|
||
const areaCode = randomElement(province.area_codes);
|
||
const phone = `(${areaCode}) ${randomInt(200, 999)}-${randomInt(1000, 9999)}`;
|
||
|
||
// Generate email
|
||
const email = generateEmail(firstName, lastName);
|
||
|
||
// Generate address
|
||
const streetNumber = randomInt(100, 9999);
|
||
const streetName = randomElement([
|
||
'Main Street', 'Oak Avenue', 'Park Road', 'Maple Drive', 'Elm Street',
|
||
'King Street', 'Queen Street', 'Church Street', 'Market Street',
|
||
'First Street', 'Second Street', 'Third Avenue', 'Fourth Street',
|
||
'Bay Street', 'Yonge Street', 'University Avenue', 'College Street',
|
||
'Dundas Street', 'Bloor Street', 'Queen Street', 'King Street',
|
||
'River Road', 'Lake Avenue', 'Hill Street', 'Valley Drive',
|
||
'Forest Avenue', 'Garden Street', 'Rose Lane', 'Pine Street'
|
||
]);
|
||
const street = `${streetNumber} ${streetName}`;
|
||
|
||
// Generate city - use province-specific cities if available, otherwise use fallback
|
||
let city;
|
||
if (province.cities && province.cities.length > 0) {
|
||
// Use cities from the province's city list
|
||
city = randomElement(province.cities);
|
||
} else {
|
||
// Fallback: use common Canadian city names (should not happen if data is complete)
|
||
const fallbackCities = [
|
||
'Toronto', 'Vancouver', 'Montreal', 'Calgary', 'Ottawa',
|
||
'Edmonton', 'Winnipeg', 'Quebec City', 'Hamilton', 'Halifax'
|
||
];
|
||
city = randomElement(fallbackCities);
|
||
}
|
||
|
||
// Canadian postal code format: A1A 1A1 - use province-specific prefixes if available
|
||
let postcodePrefix;
|
||
if (province.postcode_prefixes && province.postcode_prefixes.length > 0) {
|
||
// Use postcode prefixes from the province's list
|
||
postcodePrefix = randomElement(province.postcode_prefixes);
|
||
} else {
|
||
// Fallback: use common Canadian postal code prefixes (should not happen if data is complete)
|
||
postcodePrefix = randomElement(['A', 'B', 'C', 'E', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'X', 'Y']);
|
||
}
|
||
const postcode = `${postcodePrefix}${randomInt(0, 9)}${randomElement(['A', 'B', 'C', 'E', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'])} ${randomInt(0, 9)}${randomElement(['A', 'B', 'C', 'E', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'])}${randomInt(0, 9)}`;
|
||
|
||
// Use English province name for full address and province field
|
||
const provinceNameEn = province.name.en || province.name.zh;
|
||
const fullAddress = `${street}, ${city}, ${provinceNameEn} ${postcode}`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street,
|
||
city,
|
||
postcode,
|
||
province: provinceNameEn, // 显示英文省份名
|
||
fullAddress,
|
||
country: 'CA'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating CA address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate Japan address
|
||
export async function generateJPAddress(selectedPrefecture = 'RANDOM') {
|
||
try {
|
||
const jpData = await loadData('data/jpData.json');
|
||
const jpNamesData = await loadData('data/jpNamesData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Filter prefectures based on selected prefecture
|
||
let availablePrefectures = {};
|
||
if (selectedPrefecture === 'RANDOM') {
|
||
// Use all prefectures
|
||
availablePrefectures = jpData.prefectures || {};
|
||
} else if (jpData.prefectures && jpData.prefectures[selectedPrefecture]) {
|
||
// Use only the selected prefecture
|
||
availablePrefectures[selectedPrefecture] = jpData.prefectures[selectedPrefecture];
|
||
} else {
|
||
// Fallback to all prefectures if invalid selection
|
||
availablePrefectures = jpData.prefectures || {};
|
||
}
|
||
|
||
// Select random prefecture from filtered prefectures
|
||
const prefectures = Object.keys(availablePrefectures);
|
||
if (prefectures.length === 0) {
|
||
throw new Error('No prefectures available for selected prefecture');
|
||
}
|
||
|
||
const prefectureKey = randomElement(prefectures);
|
||
const prefecture = availablePrefectures[prefectureKey];
|
||
|
||
// Generate name using Japanese names database (kanji, hiragana, katakana)
|
||
// Japanese surnames (姓) are almost always in kanji, rarely in hiragana/katakana
|
||
const surnameScriptTypes = ['kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'hiragana']; // 90% kanji, 10% hiragana
|
||
const surnameScriptType = randomElement(surnameScriptTypes);
|
||
const lastNames = jpNamesData.surnames[surnameScriptType] || jpNamesData.surnames.kanji;
|
||
const lastName = randomElement(lastNames);
|
||
|
||
// Japanese first names (名) can be in kanji, hiragana, or katakana
|
||
const firstNameScriptTypes = ['kanji', 'kanji', 'kanji', 'hiragana', 'katakana']; // 60% kanji, 20% hiragana, 20% katakana
|
||
const firstNameScriptType = randomElement(firstNameScriptTypes);
|
||
|
||
const isMale = Math.random() > 0.5;
|
||
|
||
// Get first name based on gender and script type
|
||
let firstName;
|
||
if (isMale) {
|
||
const maleNames = jpNamesData.firstNames.male[firstNameScriptType] || jpNamesData.firstNames.male.kanji;
|
||
firstName = randomElement(maleNames);
|
||
} else {
|
||
const femaleNames = jpNamesData.firstNames.female[firstNameScriptType] || jpNamesData.firstNames.female.kanji;
|
||
firstName = randomElement(femaleNames);
|
||
}
|
||
|
||
// Gender in Japanese
|
||
const gender = isMale ? '男性' : '女性';
|
||
|
||
// Generate phone (Japan format: 0X-XXXX-XXXX) - use prefecture-specific phone codes if available
|
||
let phoneCode;
|
||
if (prefecture && prefecture.phone_codes && prefecture.phone_codes.length > 0) {
|
||
// Use phone codes from the prefecture's list
|
||
const selectedCode = randomElement(prefecture.phone_codes);
|
||
// Remove leading 0 if present and format
|
||
phoneCode = selectedCode.toString().replace(/^0+/, '');
|
||
} else {
|
||
// Fallback: use random phone code (should not happen if data is complete)
|
||
phoneCode = randomInt(3, 9).toString();
|
||
}
|
||
const phone = `0${phoneCode}-${randomInt(1000, 9999)}-${randomInt(1000, 9999)}`;
|
||
|
||
// Generate email - use English names for email to avoid non-ASCII characters
|
||
const englishNameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
|
||
let emailFirstName, emailLastName;
|
||
if (englishNameGroup && englishNameGroup.first && englishNameGroup.last) {
|
||
const firstList = ensureNameArray(englishNameGroup.first);
|
||
const lastList = ensureNameArray(englishNameGroup.last);
|
||
// Use Japanese-style names from asian group if available
|
||
const japaneseRomajiNames = firstList.filter(n =>
|
||
['Hiroshi', 'Takeshi', 'Akira', 'Satoshi', 'Kenji', 'Taro', 'Jiro', 'Ichiro', 'Yuki', 'Ai', 'Emi', 'Yui', 'Rina', 'Miki', 'Saki', 'Nana', 'Kana', 'Mana', 'Hanako', 'Misaki', 'Sakura', 'Aya', 'Rei', 'Mai', 'Eri', 'Yuka'].includes(n)
|
||
);
|
||
const japaneseRomajiLastNames = lastList.filter(n =>
|
||
['Sato', 'Suzuki', 'Takahashi', 'Tanaka', 'Watanabe', 'Ito', 'Yamamoto', 'Nakamura', 'Kobayashi', 'Kato', 'Yoshida', 'Yamada', 'Sasaki', 'Yamaguchi', 'Matsumoto', 'Inoue', 'Kimura', 'Hayashi', 'Shimizu', 'Yamazaki', 'Mori', 'Abe', 'Ikeda', 'Hashimoto', 'Ishikawa', 'Maeda', 'Fujita', 'Ogawa', 'Goto', 'Okada'].includes(n)
|
||
);
|
||
|
||
if (japaneseRomajiNames.length > 0 && japaneseRomajiLastNames.length > 0) {
|
||
emailFirstName = randomElement(japaneseRomajiNames);
|
||
emailLastName = randomElement(japaneseRomajiLastNames);
|
||
} else {
|
||
emailFirstName = randomElement(firstList);
|
||
emailLastName = randomElement(lastList);
|
||
}
|
||
} else {
|
||
// Fallback: generate random English username
|
||
const randomNames = ['john', 'mary', 'david', 'sarah', 'michael', 'emily', 'james', 'lisa', 'robert', 'anna'];
|
||
emailFirstName = randomElement(randomNames);
|
||
emailLastName = randomElement(randomNames);
|
||
}
|
||
const email = generateEmail(emailFirstName, emailLastName);
|
||
|
||
// Generate address using real Japanese address data
|
||
// Use kanji for addresses (most common in real addresses)
|
||
const addressData = jpData.address_data || {};
|
||
|
||
// City - use prefecture-specific cities if available, otherwise use fallback
|
||
let city;
|
||
if (prefecture && prefecture.cities && prefecture.cities.length > 0) {
|
||
// Use cities from the prefecture's city list
|
||
city = randomElement(prefecture.cities);
|
||
} else {
|
||
// Fallback: use common Japanese city names (should not happen if data is complete)
|
||
const fallbackCities = addressData.cities?.kanji || ['東京', '大阪', '京都', '横浜', '名古屋', '札幌', '福岡', '神戸', '仙台', '広島', '千葉', '埼玉', '新潟', '静岡', '浜松', '岡山', '熊本', '鹿児島', '長崎', '大分'];
|
||
city = randomElement(fallbackCities);
|
||
}
|
||
|
||
// Ward/丁目 (use kanji)
|
||
const wards = addressData.wards?.kanji || ['1丁目', '2丁目', '3丁目', '4丁目', '5丁目', '6丁目', '7丁目', '8丁目', '9丁目', '10丁目'];
|
||
const ward = randomElement(wards);
|
||
|
||
// Street name (use kanji from real street data)
|
||
const streets = addressData.streets?.kanji || ['中央', '本町', '新町', '大通', '駅前', '公園', '桜', '松', '竹', '梅', '富士', '山', '川', '海', '森', '田', '橋', '坂', '谷', '原'];
|
||
const streetName = randomElement(streets);
|
||
|
||
// District (区/市) - optional, sometimes included
|
||
const includeDistrict = Math.random() > 0.7; // 30% chance to include district
|
||
let district = '';
|
||
if (includeDistrict && addressData.districts?.kanji) {
|
||
district = randomElement(addressData.districts.kanji) + '';
|
||
}
|
||
|
||
const streetNumber = randomInt(1, 50);
|
||
const buildingNumber = randomInt(1, 20);
|
||
|
||
// Build address: [district] [street] [ward] [number]番[building]号
|
||
let address;
|
||
if (district) {
|
||
address = `${district}${streetName}${ward}${streetNumber}番${buildingNumber}号`;
|
||
} else {
|
||
address = `${streetName}${ward}${streetNumber}番${buildingNumber}号`;
|
||
}
|
||
|
||
// Japanese postal code format: 123-4567 (使用都道府県的邮编前缀)
|
||
let postcodePrefix = '100';
|
||
if (prefecture && prefecture.postal_prefix && prefecture.postal_prefix.length > 0) {
|
||
postcodePrefix = randomElement(prefecture.postal_prefix);
|
||
}
|
||
const postcode = `${postcodePrefix}-${randomInt(1000, 9999)}`;
|
||
|
||
// 使用日文都道府県名称
|
||
const prefectureName = prefecture ? (prefecture.name.ja || prefecture.name.zh) : '東京都';
|
||
const fullAddress = `〒${postcode} ${prefectureName}${city}${address}`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street: address,
|
||
city,
|
||
prefecture: prefectureName,
|
||
postcode,
|
||
fullAddress,
|
||
country: 'JP'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating JP address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate India address
|
||
export async function generateINAddress(selectedState = 'RANDOM') {
|
||
try {
|
||
const inData = await loadData('data/inData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Filter states based on selected state
|
||
let availableStates = {};
|
||
if (selectedState === 'RANDOM') {
|
||
// Use all states
|
||
availableStates = inData.states;
|
||
} else if (inData.states[selectedState]) {
|
||
// Use only the selected state
|
||
availableStates[selectedState] = inData.states[selectedState];
|
||
} else {
|
||
// Fallback to all states if invalid selection
|
||
availableStates = inData.states;
|
||
}
|
||
|
||
// Select random state from filtered states
|
||
const states = Object.keys(availableStates);
|
||
if (states.length === 0) {
|
||
throw new Error('No states available for selected state');
|
||
}
|
||
|
||
const stateKey = randomElement(states);
|
||
const state = availableStates[stateKey];
|
||
|
||
// Generate name (Indian) - decide gender first, then select name
|
||
const nameGroup = namesData.nameGroups.indian;
|
||
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
|
||
// Select name based on gender - use gender-specific name lists if available
|
||
let firstName;
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
// Use gender-specific name lists
|
||
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
|
||
} else {
|
||
// Fallback: use all names if gender classification not available
|
||
firstName = randomElement(nameGroup.first);
|
||
}
|
||
const lastName = randomElement(nameGroup.last);
|
||
|
||
// Generate phone (India format: +91 XXXXXXXXXX) - use state-specific area codes if available
|
||
let phoneNumber;
|
||
if (state.area_codes && state.area_codes.length > 0) {
|
||
// Use area codes from the state's list (format: XXXX, convert to phone number)
|
||
const areaCode = randomElement(state.area_codes);
|
||
// Indian mobile numbers start with 6-9, generate remaining digits
|
||
const mobilePrefix = randomElement([6, 7, 8, 9]);
|
||
const remainingDigits = randomInt(10000000, 99999999);
|
||
phoneNumber = `${mobilePrefix}${remainingDigits}`;
|
||
} else {
|
||
// Fallback: use random phone number (should not happen if data is complete)
|
||
phoneNumber = randomInt(6000000000, 9999999999).toString();
|
||
}
|
||
const phone = `+91 ${phoneNumber}`;
|
||
|
||
// Generate email
|
||
const email = generateEmail(firstName, lastName);
|
||
|
||
// Generate address
|
||
const streetNumber = randomInt(1, 999);
|
||
const streetName = randomElement([
|
||
'Main Road', 'Gandhi Street', 'Nehru Road', 'Park Street', 'Market Road',
|
||
'Church Street', 'Temple Street', 'School Road', 'Hospital Road',
|
||
'MG Road', 'Station Road', 'Airport Road', 'Highway Road', 'Ring Road',
|
||
'First Street', 'Second Street', 'Third Street', 'Fourth Street',
|
||
'Gandhi Nagar', 'Nehru Nagar', 'Rajiv Nagar', 'Indira Nagar',
|
||
'College Road', 'University Road', 'Library Road', 'Museum Road'
|
||
]);
|
||
const street = `${streetNumber}, ${streetName}`;
|
||
|
||
// Generate city - use state-specific cities if available, otherwise use fallback
|
||
let city;
|
||
if (state.cities && state.cities.length > 0) {
|
||
// Use cities from the state's city list
|
||
city = randomElement(state.cities);
|
||
} else {
|
||
// Fallback: use common Indian city names (should not happen if data is complete)
|
||
const fallbackCities = [
|
||
'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Chennai',
|
||
'Kolkata', 'Pune', 'Ahmedabad', 'Jaipur', 'Surat'
|
||
];
|
||
city = randomElement(fallbackCities);
|
||
}
|
||
|
||
// Indian PIN code (6 digits) - use state-specific PIN range if available
|
||
let pin;
|
||
if (state.pin_range && state.pin_range.min && state.pin_range.max) {
|
||
// Use PIN code range from the state's pin_range
|
||
pin = randomInt(state.pin_range.min, state.pin_range.max).toString();
|
||
} else {
|
||
// Fallback: use random 6-digit PIN code (should not happen if data is complete)
|
||
pin = randomInt(100000, 999999).toString();
|
||
}
|
||
|
||
// Use English state name for full address and state field
|
||
const stateNameEn = state.name.en || state.name.zh;
|
||
const fullAddress = `${street}, ${city}, ${stateNameEn} ${pin}`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street,
|
||
city,
|
||
state: stateNameEn, // 显示英文邦名
|
||
pin,
|
||
fullAddress,
|
||
country: 'IN'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating IN address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate Taiwan address
|
||
export async function generateTWAddress(selectedCounty = 'RANDOM') {
|
||
try {
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Try to load Taiwan data
|
||
let twData = null;
|
||
let selectedCountyData = null;
|
||
try {
|
||
twData = await loadData('data/twData.json');
|
||
if (twData && twData.counties) {
|
||
// Filter counties based on selected county
|
||
let availableCounties = {};
|
||
if (selectedCounty === 'RANDOM') {
|
||
// Use all counties
|
||
availableCounties = twData.counties;
|
||
} else if (twData.counties[selectedCounty]) {
|
||
// Use only the selected county
|
||
availableCounties[selectedCounty] = twData.counties[selectedCounty];
|
||
} else {
|
||
// Fallback to all counties if invalid selection
|
||
availableCounties = twData.counties;
|
||
}
|
||
|
||
// Select random county from filtered counties
|
||
const counties = Object.keys(availableCounties);
|
||
if (counties.length > 0) {
|
||
const countyKey = randomElement(counties);
|
||
selectedCountyData = availableCounties[countyKey];
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// If data file doesn't exist, use fallback
|
||
console.warn('Taiwan data file not found, using fallback');
|
||
}
|
||
|
||
// Generate name (Chinese) - decide gender first, then select name
|
||
const nameGroup = namesData.nameGroups.chinese;
|
||
const gender = Math.random() > 0.5 ? '男' : '女'; // 中文:男/女
|
||
const firstName = randomElement(gender === '男' ? nameGroup.first.male : nameGroup.first.female);
|
||
const lastName = randomElement(nameGroup.last);
|
||
|
||
// Generate phone (Taiwan format: 09XX-XXX-XXX) - use county-specific area codes if available
|
||
let phoneAreaCode;
|
||
if (selectedCountyData && selectedCountyData.phone_area_codes && selectedCountyData.phone_area_codes.length > 0) {
|
||
// Use phone area codes from the county's list
|
||
phoneAreaCode = randomElement(selectedCountyData.phone_area_codes);
|
||
} else {
|
||
// Fallback: use random area code (should not happen if data is complete)
|
||
phoneAreaCode = randomInt(2, 9);
|
||
}
|
||
const phone = `09${phoneAreaCode}${randomInt(10, 99)}-${randomInt(100, 999)}-${randomInt(100, 999)}`;
|
||
|
||
// Generate email - use English names for email to avoid Chinese characters
|
||
const englishNameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
|
||
let emailFirstName, emailLastName;
|
||
if (englishNameGroup && englishNameGroup.first && englishNameGroup.last) {
|
||
const firstList = ensureNameArray(englishNameGroup.first);
|
||
const lastList = ensureNameArray(englishNameGroup.last);
|
||
emailFirstName = randomElement(firstList);
|
||
emailLastName = randomElement(lastList);
|
||
} else {
|
||
// Fallback: generate random English username
|
||
const randomNames = ['john', 'mary', 'david', 'sarah', 'michael', 'emily', 'james', 'lisa', 'robert', 'anna'];
|
||
emailFirstName = randomElement(randomNames);
|
||
emailLastName = randomElement(randomNames);
|
||
}
|
||
const email = generateEmail(emailFirstName, emailLastName);
|
||
|
||
// Generate address
|
||
// Use county data if available, otherwise use fallback cities
|
||
let city, district;
|
||
if (selectedCountyData && selectedCountyData.name) {
|
||
city = selectedCountyData.name.zh;
|
||
// Use common districts for the selected city
|
||
district = randomElement(['中正區', '大同區', '中山區', '松山區', '大安區', '萬華區', '信義區', '士林區', '北投區', '內湖區', '南港區', '文山區']);
|
||
} else {
|
||
// Fallback cities
|
||
const cities = ['台北市', '新北市', '台中市', '台南市', '高雄市', '桃園市'];
|
||
city = randomElement(cities);
|
||
district = randomElement(['中正區', '大同區', '中山區', '松山區', '大安區', '萬華區']);
|
||
}
|
||
|
||
const street = randomElement([
|
||
'中山路', '中正路', '民生路', '民權路', '民族路',
|
||
'建國路', '復興路', '和平路', '自由路', '成功路',
|
||
'忠孝路', '仁愛路', '信義路', '和平路', '光復路',
|
||
'中華路', '文化路', '大學路', '公園路', '車站路',
|
||
'中央路', '大同路', '大安路', '大業路', '大興路',
|
||
'新生路', '新興路', '新市路', '新莊路', '新店路'
|
||
]);
|
||
const streetNumber = randomInt(1, 999);
|
||
const address = `${city}${district}${street}${streetNumber}號`;
|
||
|
||
// Taiwan postal code (5 digits) - use county-specific postcode range if available
|
||
let postcode;
|
||
if (selectedCountyData && selectedCountyData.postcode_range && selectedCountyData.postcode_range.min && selectedCountyData.postcode_range.max) {
|
||
// Use postcode range from the county's postcode_range
|
||
postcode = randomInt(selectedCountyData.postcode_range.min, selectedCountyData.postcode_range.max).toString();
|
||
} else {
|
||
// Fallback: use random 5-digit postcode (should not happen if data is complete)
|
||
postcode = randomInt(10000, 99999).toString();
|
||
}
|
||
|
||
const fullAddress = `${address}, 郵遞區號: ${postcode}`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street: address,
|
||
city,
|
||
district,
|
||
postcode,
|
||
fullAddress,
|
||
country: 'TW'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating TW address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate tax-free US address (only from tax-free states)
|
||
export async function generateTaxFreeAddress(selectedState = 'DE') {
|
||
const taxFreeStates = ['AK', 'DE', 'MT', 'NH', 'OR'];
|
||
|
||
if (!taxFreeStates.includes(selectedState)) {
|
||
selectedState = randomElement(taxFreeStates);
|
||
}
|
||
|
||
return await generateUSAddress(selectedState);
|
||
}
|
||
|
||
// Generate identity information
|
||
export async function generateIdentityInfo(address) {
|
||
try {
|
||
// Load names data - use the same loadData function which handles paths correctly
|
||
const namesData = await loadData('data/namesData.json');
|
||
if (!namesData || !namesData.nameGroups) {
|
||
throw new Error('Names data not available');
|
||
}
|
||
|
||
// Use the same name group as the address based on country
|
||
const c = address.country || '';
|
||
let nameGroup;
|
||
if (c === '香港' || c === '台灣' || c === 'HK' || c === 'TW') {
|
||
nameGroup = namesData.nameGroups.chinese;
|
||
} else if (c === '印度' || c === 'IN') {
|
||
nameGroup = namesData.nameGroups.indian;
|
||
} else if (c === '日本' || c === 'JP') {
|
||
nameGroup = namesData.nameGroups.asian || namesData.nameGroups.western;
|
||
} else {
|
||
// Default to western names for US, UK, Canada, etc.
|
||
nameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
|
||
}
|
||
if (!nameGroup) {
|
||
throw new Error('Name group not found');
|
||
}
|
||
|
||
// Resolve gender for identity (name selection + Taiwan ID gender digit)
|
||
let isMaleForIdentity;
|
||
if (address.gender) {
|
||
const g = address.gender;
|
||
const isMale = g === 'Male' || g === 'male' || g === 'm' || g === '男' || g === '男性' ||
|
||
(typeof g === 'string' && (g.includes('Männlich') || g.includes('男')));
|
||
const isFemale = g === 'Female' || g === 'female' || g === 'f' || g === '女' || g === '女性' ||
|
||
(typeof g === 'string' && (g.includes('Weiblich') || g.includes('女')));
|
||
isMaleForIdentity = isMale ? true : (isFemale ? false : Math.random() > 0.5);
|
||
} else {
|
||
isMaleForIdentity = Math.random() > 0.5;
|
||
}
|
||
|
||
// Generate name based on address gender if available
|
||
let firstName, lastName;
|
||
if (address.gender) {
|
||
const genderKey = address.gender.toLowerCase();
|
||
const isMale = genderKey === 'male' || genderKey === 'm' || address.gender === '男' || address.gender === '男性' ||
|
||
address.gender.includes('Männlich') || address.gender.includes('男');
|
||
const isFemale = genderKey === 'female' || genderKey === 'f' || address.gender === '女' || address.gender === '女性' ||
|
||
address.gender.includes('Weiblich') || address.gender.includes('女');
|
||
|
||
// Use gender-specific name lists if available
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
if (isMale) {
|
||
firstName = randomElement(nameGroup.first.male);
|
||
} else if (isFemale) {
|
||
firstName = randomElement(nameGroup.first.female);
|
||
} else {
|
||
// Fallback: randomly choose from either gender
|
||
firstName = randomElement(Math.random() > 0.5 ? nameGroup.first.male : nameGroup.first.female);
|
||
}
|
||
} else {
|
||
// Fallback: use all names if gender classification not available
|
||
firstName = randomElement(Array.isArray(nameGroup.first) ? nameGroup.first : []);
|
||
}
|
||
} else {
|
||
// If no gender specified, randomly choose gender and corresponding name
|
||
const randomGender = Math.random() > 0.5;
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
firstName = randomElement(randomGender ? nameGroup.first.male : nameGroup.first.female);
|
||
} else {
|
||
firstName = randomElement(Array.isArray(nameGroup.first) ? nameGroup.first : []);
|
||
}
|
||
}
|
||
lastName = randomElement(nameGroup.last);
|
||
|
||
// Generate date of birth (age between 20 and 50)
|
||
const age = randomInt(20, 50);
|
||
const birthYear = new Date().getFullYear() - age;
|
||
const birthMonth = randomInt(1, 12);
|
||
const daysInMonth = new Date(birthYear, birthMonth, 0).getDate();
|
||
const birthDay = randomInt(1, daysInMonth);
|
||
const dateOfBirth = `${birthMonth.toString().padStart(2, '0')}/${birthDay.toString().padStart(2, '0')}/${birthYear}`;
|
||
|
||
// Generate identity ID based on country
|
||
let ssn;
|
||
const country = address.country || '';
|
||
|
||
if (country.includes('德国') || country.includes('Germany') || country === 'DE') {
|
||
// Generate German Steuer-ID (Tax ID): 11 digits, format: XX XXX XXX XXX
|
||
const part1 = randomInt(10, 99);
|
||
const part2 = randomInt(100, 999);
|
||
const part3 = randomInt(100, 999);
|
||
const part4 = randomInt(100, 999);
|
||
ssn = `${part1} ${part2} ${part3} ${part4}`;
|
||
} else if (country.includes('英国') || country.includes('UK') || country.includes('United Kingdom') || country === 'UK') {
|
||
// Generate UK NINO: Format: AA 12 34 56 A
|
||
const letters1 = 'ABCDEFGHJKLMNOPRSTUVWXYZ';
|
||
const letters2 = 'ABCDEFGHJKLMNOPRSTUVWXYZ';
|
||
const letter1 = randomElement(letters1.split(''));
|
||
const letter2 = randomElement(letters2.split(''));
|
||
const digits = randomInt(100000, 999999);
|
||
const letter3 = randomElement(letters2.split(''));
|
||
ssn = `${letter1}${letter2} ${digits.toString().slice(0, 2)} ${digits.toString().slice(2, 4)} ${digits.toString().slice(4, 6)} ${letter3}`;
|
||
} else if (country.includes('加拿大') || country.includes('Canada') || country === 'CA') {
|
||
// Generate Canadian SIN: Format: XXX XXX XXX
|
||
const sin1 = randomInt(100, 999);
|
||
const sin2 = randomInt(100, 999);
|
||
const sin3 = randomInt(100, 999);
|
||
ssn = `${sin1} ${sin2} ${sin3}`;
|
||
} else if (country.includes('日本') || country.includes('Japan') || country === 'JP') {
|
||
// Generate Japanese My Number: Format: XXXX-XXXX-XXXX
|
||
const myNum1 = randomInt(1000, 9999);
|
||
const myNum2 = randomInt(1000, 9999);
|
||
const myNum3 = randomInt(1000, 9999);
|
||
ssn = `${myNum1}-${myNum2}-${myNum3}`;
|
||
} else if (country.includes('印度') || country.includes('India') || country === 'IN') {
|
||
// Generate Indian Aadhaar: Format: XXXX XXXX XXXX
|
||
const aadhaar1 = randomInt(1000, 9999);
|
||
const aadhaar2 = randomInt(1000, 9999);
|
||
const aadhaar3 = randomInt(1000, 9999);
|
||
ssn = `${aadhaar1} ${aadhaar2} ${aadhaar3}`;
|
||
} else if (country.includes('香港') || country.includes('Hong Kong') || country === 'HK') {
|
||
// Generate Hong Kong ID Card: Format: A123456(7) or AB123456(7)
|
||
// 70%概率单字母,30%概率双字母
|
||
const letters = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||
const isDoubleLetter = Math.random() < 0.3;
|
||
let prefix;
|
||
if (isDoubleLetter) {
|
||
const letter1 = randomElement(letters.split(''));
|
||
const letter2 = randomElement(letters.split(''));
|
||
prefix = `${letter1}${letter2}`;
|
||
} else {
|
||
prefix = randomElement(letters.split(''));
|
||
}
|
||
const digits = randomInt(100000, 999999).toString();
|
||
const checkDigit = randomInt(0, 9);
|
||
ssn = `${prefix}${digits}(${checkDigit})`;
|
||
} else if (country.includes('台灣') || country.includes('台湾') || country.includes('Taiwan') || country === 'TW') {
|
||
// Generate Taiwan ID Card: Format: A123456789
|
||
// 1st: letter (birthplace), 2nd: gender (1=Male, 2=Female), 3rd-9th: sequence
|
||
const letters = 'ABCDEFGHJKLMNPQRSTUVXY';
|
||
const firstLetter = randomElement(letters.split(''));
|
||
const genderDigit = isMaleForIdentity ? '1' : '2'; // 1=男, 2=女
|
||
const sequenceDigits = randomInt(10000000, 99999999).toString();
|
||
ssn = `${firstLetter}${genderDigit}${sequenceDigits}`;
|
||
} else if (country.includes('新加坡') || country.includes('Singapore') || country === 'SG') {
|
||
// Generate Singapore NRIC: Format: S1234567D (prefix + 7 digits + check letter)
|
||
// S=citizen pre-2000, T=citizen 2000+, G/F=PR. Prefix should match birth year
|
||
let prefix;
|
||
if (birthYear < 2000) {
|
||
prefix = 'S'; // Born before 2000
|
||
} else {
|
||
prefix = 'T'; // Born in 2000 or later
|
||
}
|
||
// Small chance to be PR (G prefix) regardless of year
|
||
if (Math.random() < 0.1) {
|
||
prefix = 'G'; // Permanent Resident
|
||
}
|
||
const digits = randomInt(1000000, 9999999).toString();
|
||
const checkLetters = 'ABCDEFGHIZJ';
|
||
const checkLetter = randomElement(checkLetters.split(''));
|
||
ssn = `${prefix}${digits}${checkLetter}`;
|
||
} else {
|
||
// Default: US SSN format (XXX-XX-XXXX)
|
||
// SSN Area Number (first 3 digits) should match the state if available
|
||
let ssnAreaNumber;
|
||
if (address.stateCode && (address.country === '美国' || address.country === 'US')) {
|
||
try {
|
||
const usData = await loadData('data/usData.json');
|
||
const state = usData.states[address.stateCode];
|
||
if (state && state.ssn_area_range && state.ssn_area_range.min && state.ssn_area_range.max) {
|
||
// Use state-specific SSN area number range
|
||
ssnAreaNumber = randomInt(state.ssn_area_range.min, state.ssn_area_range.max);
|
||
} else {
|
||
// Fallback: use random area number (avoid 000, 666, and 900-999)
|
||
do {
|
||
ssnAreaNumber = randomInt(1, 899);
|
||
} while (ssnAreaNumber === 666 || ssnAreaNumber < 1);
|
||
}
|
||
} catch (e) {
|
||
// If loading fails, use fallback
|
||
do {
|
||
ssnAreaNumber = randomInt(1, 899);
|
||
} while (ssnAreaNumber === 666 || ssnAreaNumber < 1);
|
||
}
|
||
} else {
|
||
// For non-US addresses or if stateCode is not available, use random area number
|
||
do {
|
||
ssnAreaNumber = randomInt(1, 899);
|
||
} while (ssnAreaNumber === 666 || ssnAreaNumber < 1);
|
||
}
|
||
|
||
// Generate Group Number (middle 2 digits, cannot be 00)
|
||
const groupNumber = randomInt(1, 99);
|
||
|
||
// Generate Serial Number (last 4 digits, cannot be 0000)
|
||
const serialNumber = randomInt(1, 9999);
|
||
|
||
// Format SSN: XXX-XX-XXXX
|
||
ssn = `${ssnAreaNumber.toString().padStart(3, '0')}-${groupNumber.toString().padStart(2, '0')}-${serialNumber.toString().padStart(4, '0')}`;
|
||
}
|
||
|
||
// Generate occupation (random job title)
|
||
const occupations = [
|
||
'Software Engineer', 'Teacher', 'Doctor', 'Nurse', 'Engineer',
|
||
'Accountant', 'Lawyer', 'Manager', 'Sales Representative', 'Designer',
|
||
'Marketing Specialist', 'Consultant', 'Analyst', 'Administrator', 'Director',
|
||
'Developer', 'Architect', 'Coordinator', 'Supervisor', 'Assistant'
|
||
];
|
||
const occupation = randomElement(occupations);
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
dateOfBirth,
|
||
age,
|
||
ssn,
|
||
occupation
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating identity info:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate credit card information
|
||
export async function generateCreditCardInfo() {
|
||
// Card types with their prefixes
|
||
const cardTypes = [
|
||
{ name: 'Visa', prefixes: ['4'] },
|
||
{ name: 'MasterCard', prefixes: ['51', '52', '53', '54', '55'] },
|
||
{ name: 'American Express', prefixes: ['34', '37'] },
|
||
{ name: 'Discover', prefixes: ['6011'] }
|
||
];
|
||
|
||
// Select random card type
|
||
const selectedCardType = randomElement(cardTypes);
|
||
const prefix = randomElement(selectedCardType.prefixes);
|
||
|
||
// Generate random credit card number (16 digits, Luhn algorithm)
|
||
function generateLuhnNumber(prefix, length = 16) {
|
||
let cardNumber = prefix;
|
||
// Generate remaining digits (excluding prefix and check digit)
|
||
const remainingDigits = length - prefix.length - 1;
|
||
for (let i = 0; i < remainingDigits; i++) {
|
||
cardNumber += randomInt(0, 9).toString();
|
||
}
|
||
|
||
// Calculate check digit using Luhn algorithm
|
||
let sum = 0;
|
||
let double = false;
|
||
for (let i = cardNumber.length - 1; i >= 0; i--) {
|
||
let digit = parseInt(cardNumber[i]);
|
||
if (double) {
|
||
digit *= 2;
|
||
if (digit > 9) digit -= 9;
|
||
}
|
||
sum += digit;
|
||
double = !double;
|
||
}
|
||
const checkDigit = (10 - (sum % 10)) % 10;
|
||
cardNumber += checkDigit.toString();
|
||
|
||
return cardNumber;
|
||
}
|
||
|
||
// Generate card number (16 digits for most cards, 15 for Amex)
|
||
const cardLength = selectedCardType.name === 'American Express' ? 15 : 16;
|
||
const cardNumber = generateLuhnNumber(prefix, cardLength);
|
||
|
||
// Format as XXXX XXXX XXXX XXXX (or XXXX XXXXXX XXXXX for Amex)
|
||
const formattedCardNumber = cardNumber.match(/.{1,4}/g).join(' ');
|
||
|
||
// Generate expiration date (future date, 1-5 years from now)
|
||
const currentYear = new Date().getFullYear();
|
||
const expYear = currentYear + randomInt(1, 5);
|
||
const expMonth = randomInt(1, 12);
|
||
const expirationDate = `${expMonth.toString().padStart(2, '0')}/${expYear.toString().slice(-2)}`;
|
||
|
||
// Generate CVV (3 digits for most cards, 4 for Amex)
|
||
const cvvLength = selectedCardType.name === 'American Express' ? 4 : 3;
|
||
const cvv = randomInt(Math.pow(10, cvvLength - 1), Math.pow(10, cvvLength) - 1).toString();
|
||
|
||
// Generate cardholder name (random Western name)
|
||
const names = ['John', 'Mary', 'David', 'Sarah', 'Michael', 'Emily', 'James', 'Lisa', 'Robert', 'Anna'];
|
||
const surnames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez'];
|
||
const cardholderName = `${randomElement(names)} ${randomElement(surnames)}`;
|
||
|
||
return {
|
||
type: selectedCardType.name,
|
||
number: formattedCardNumber,
|
||
rawNumber: cardNumber,
|
||
expiryDate: expirationDate,
|
||
expirationDate: expirationDate,
|
||
cvv,
|
||
cardholderName
|
||
};
|
||
}
|
||
|
||
// Generate Singapore address
|
||
export async function generateSGAddress(selectedState = 'RANDOM') {
|
||
try {
|
||
const sgData = await loadData('data/sgData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
let availableStates = {};
|
||
if (selectedState === 'RANDOM') {
|
||
availableStates = sgData.states;
|
||
} else if (sgData.states[selectedState]) {
|
||
availableStates[selectedState] = sgData.states[selectedState];
|
||
} else {
|
||
availableStates = sgData.states;
|
||
}
|
||
|
||
const states = Object.keys(availableStates);
|
||
if (states.length === 0) throw new Error('No regions available');
|
||
|
||
const stateKey = randomElement(states);
|
||
const state = availableStates[stateKey];
|
||
|
||
const nameGroup = namesData.nameGroups.western;
|
||
const genderRaw = Math.random() > 0.5 ? 'Male' : 'Female';
|
||
let firstName;
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
firstName = randomElement(genderRaw === 'Male' ? nameGroup.first.male : nameGroup.first.female);
|
||
} else {
|
||
firstName = randomElement(nameGroup.first);
|
||
}
|
||
const lastName = randomElement(nameGroup.last);
|
||
const gender = genderRaw === 'Male' ? 'Male' : 'Female';
|
||
|
||
const mobilePrefix = randomElement(['8', '9']);
|
||
const subscriberNumber = randomInt(1000000, 9999999);
|
||
const phone = `+65 ${mobilePrefix}${subscriberNumber}`;
|
||
|
||
const email = generateEmail(firstName, lastName);
|
||
|
||
const cities = Object.keys(state.cities);
|
||
if (cities.length === 0) throw new Error(`No cities for region ${stateKey}`);
|
||
|
||
const cityName = randomElement(cities);
|
||
const cityData = state.cities[cityName];
|
||
|
||
if (!cityData || !cityData.zip || !cityData.streets) {
|
||
throw new Error(`Invalid city data for ${cityName}`);
|
||
}
|
||
|
||
const zip = randomElement(cityData.zip);
|
||
const streetName = randomElement(cityData.streets);
|
||
|
||
const useBlockFormat = Math.random() > 0.3;
|
||
let street;
|
||
if (useBlockFormat) {
|
||
const blockNum = randomInt(1, 999);
|
||
const floor = randomInt(1, 25);
|
||
const unit = randomInt(1, 12);
|
||
street = `Block ${blockNum}, ${streetName}, #${floor.toString().padStart(2, '0')}-${unit.toString().padStart(2, '0')}`;
|
||
} else {
|
||
const unitNum = randomInt(1, 200);
|
||
const floor = randomInt(1, 30);
|
||
const unit = randomInt(1, 8);
|
||
street = `${unitNum} ${streetName}, #${floor}-${unit}`;
|
||
}
|
||
|
||
const stateNameEn = state.name.en || state.name.zh;
|
||
const fullAddress = `${street}, Singapore ${zip}`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street,
|
||
city: cityName,
|
||
zip,
|
||
postcode: zip,
|
||
state: stateNameEn,
|
||
stateCode: stateKey,
|
||
fullAddress,
|
||
country: 'SG'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating SG address:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Generate Germany address
|
||
export async function generateDEAddress(selectedState = 'RANDOM') {
|
||
try {
|
||
const deData = await loadData('data/deData.json');
|
||
const namesData = await loadData('data/namesData.json');
|
||
|
||
// Filter states based on selected state
|
||
let availableStates = {};
|
||
if (selectedState === 'RANDOM') {
|
||
// Use all states
|
||
availableStates = deData.states;
|
||
} else if (deData.states[selectedState]) {
|
||
// Use only the selected state
|
||
availableStates[selectedState] = deData.states[selectedState];
|
||
} else {
|
||
// Fallback to all states if invalid selection
|
||
availableStates = deData.states;
|
||
}
|
||
|
||
// Select random state from filtered states
|
||
const states = Object.keys(availableStates);
|
||
if (states.length === 0) {
|
||
throw new Error('No states available for selected state');
|
||
}
|
||
|
||
const stateKey = randomElement(states);
|
||
const state = availableStates[stateKey];
|
||
|
||
// Generate name - decide gender first, then select name
|
||
const nameGroup = namesData.nameGroups.western;
|
||
const genderRaw = Math.random() > 0.5 ? 'Male' : 'Female';
|
||
// Select name based on gender - use gender-specific name lists if available
|
||
let firstName;
|
||
if (nameGroup.first.male && nameGroup.first.female) {
|
||
// Use gender-specific name lists
|
||
firstName = randomElement(genderRaw === 'Male' ? nameGroup.first.male : nameGroup.first.female);
|
||
} else {
|
||
// Fallback: use all names if gender classification not available
|
||
firstName = randomElement(nameGroup.first);
|
||
}
|
||
const lastName = randomElement(nameGroup.last);
|
||
|
||
// Convert gender to German format for display: Männlich (男) / Weiblich (女)
|
||
const gender = genderRaw === 'Male' ? 'Männlich (男)' : 'Weiblich (女)';
|
||
|
||
// Generate phone - German mobile format: +49 1xx xxxxxxx
|
||
// Mobile prefixes: 151, 160, 170, 171, 175 (Telekom); 152, 162, 172, 173, 174 (Vodafone); 157, 163, 176, 177, 178 (O2)
|
||
const mobilePrefixes = ['151', '160', '170', '171', '175', '152', '162', '172', '173', '174', '157', '163', '176', '177', '178'];
|
||
const prefix = randomElement(mobilePrefixes);
|
||
const subscriberNumber = randomInt(1000000, 9999999);
|
||
const phone = `+49 ${prefix} ${subscriberNumber}`;
|
||
|
||
// Generate email - use generateEmail function (ensures no dot before @)
|
||
const email = generateEmail(firstName, lastName);
|
||
|
||
// Generate address - select random city from state
|
||
// IMPORTANT: Ensure city is selected from the correct state to avoid cross-state mismatches
|
||
const cities = Object.keys(state.cities);
|
||
if (cities.length === 0) {
|
||
throw new Error(`No cities available for state ${stateKey}`);
|
||
}
|
||
|
||
const cityName = randomElement(cities);
|
||
const cityData = state.cities[cityName];
|
||
|
||
// Validate city data structure
|
||
if (!cityData || !cityData.zip || !cityData.streets) {
|
||
throw new Error(`Invalid city data structure for ${cityName} in state ${stateKey}`);
|
||
}
|
||
|
||
// Get zip code from city data (ensures zip matches the city)
|
||
if (cityData.zip.length === 0) {
|
||
throw new Error(`No zip codes available for city ${cityName} in state ${stateKey}`);
|
||
}
|
||
const zip = randomElement(cityData.zip);
|
||
|
||
// Get street name from city data (ensures street matches the city)
|
||
if (cityData.streets.length === 0) {
|
||
throw new Error(`No streets available for city ${cityName} in state ${stateKey}`);
|
||
}
|
||
const streetName = randomElement(cityData.streets);
|
||
|
||
// Generate house number (1-150, occasionally with letter suffix like 12a)
|
||
const houseNumber = randomInt(1, 150);
|
||
const suffix = Math.random() < 0.2 ? String.fromCharCode(97 + randomInt(0, 2)) : '';
|
||
const street = `${streetName} ${houseNumber}${suffix}`;
|
||
|
||
// Use English state name for full address and state field
|
||
const stateNameEn = state.name.en || state.name.zh;
|
||
const fullAddress = `${street}, ${zip} ${cityName}, ${stateNameEn}, Germany`;
|
||
|
||
return {
|
||
firstName,
|
||
lastName,
|
||
gender,
|
||
phone,
|
||
email,
|
||
street,
|
||
city: cityName,
|
||
zip,
|
||
postcode: zip,
|
||
state: stateNameEn,
|
||
stateCode: stateKey,
|
||
fullAddress,
|
||
country: 'DE'
|
||
};
|
||
} catch (error) {
|
||
console.error('Error generating DE address:', error);
|
||
throw error;
|
||
}
|
||
}
|