1
2
3
4
5
6
7
8import http from 'node:http';
9import { Actor, log } from 'apify';
10import { preprocess } from './preprocessor.js';
11
12
13
14
15
16
17function normalizeItems(items) {
18 return items.map((item) => {
19 if (typeof item === 'string') {
20 return item;
21 }
22 if (item && typeof item === 'object' && 'input' in item) {
23 return String(item.input ?? '');
24 }
25 return String(item ?? '');
26 });
27}
28
29await Actor.init();
30
31if (Actor.config.get('metaOrigin') === 'STANDBY') {
32 startStandbyServer();
33} else {
34 await runBatchMode();
35 await Actor.exit();
36}
37
38
39
40
41
42function startStandbyServer() {
43 const port = Actor.config.get('containerPort');
44
45 const server = http.createServer(async (req, res) => {
46
47 if (req.headers['x-apify-container-server-readiness-probe']) {
48 res.writeHead(200, { 'Content-Type': 'application/json' });
49 res.end(JSON.stringify({ status: 'ready' }));
50 return;
51 }
52
53
54 if (req.method !== 'GET') {
55 res.writeHead(405, { 'Content-Type': 'application/json' });
56 res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
57 return;
58 }
59
60
61 const reqUrl = new URL(req.url, `http://${req.headers.host}`);
62 if (!reqUrl.searchParams.get('input')) {
63 res.writeHead(200, { 'Content-Type': 'text/html' });
64 res.end(getLandingPageHtml());
65 return;
66 }
67
68 try {
69 const result = handleStandbyRequest(reqUrl);
70 await Actor.pushData(result);
71
72 res.writeHead(200, { 'Content-Type': 'application/json' });
73 res.end(JSON.stringify(result));
74 } catch (err) {
75 const statusCode = err.statusCode || 500;
76 res.writeHead(statusCode, { 'Content-Type': 'application/json' });
77 res.end(JSON.stringify({ error: err.message }));
78 }
79 });
80
81 server.listen(port, () => {
82 console.log(`Superclean Places Standby server listening on port ${port}`);
83 });
84}
85
86function handleStandbyRequest(reqUrl) {
87 const input = reqUrl.searchParams.get('input');
88
89 if (!input) {
90 const err = new Error("Provide an 'input' query parameter with the location to clean.");
91 err.statusCode = 400;
92 throw err;
93 }
94
95 const style = reqUrl.searchParams.get('style') || 'short';
96
97 const validStyles = ['short', 'long', 'components'];
98 if (!validStyles.includes(style)) {
99 const err = new Error(`Invalid style "${style}". Must be one of: ${validStyles.join(', ')}`);
100 err.statusCode = 400;
101 throw err;
102 }
103
104 const result = preprocess(input, style);
105
106 return {
107 id: 1,
108 input,
109 output: result.value,
110 city: result.city,
111 stateOrProvince: result.stateOrProvince,
112 country: result.country,
113 confidence: Math.round(result.confidence * 100) / 100,
114 };
115}
116
117
118
119
120
121async function runBatchMode() {
122 const input = await Actor.getInput();
123
124 if (!input) {
125 await Actor.pushData({ error: 'no_input', message: 'No input provided. Example: {"items": ["New York, NY"]}' });
126 await Actor.exit('Skipped: No input provided. Example: {"items": ["New York, NY"]}');
127 return;
128 }
129
130 let { items: rawItems = [], item, style = 'short' } = input;
131
132
133 if (typeof item === 'string' && item.trim()) {
134 if (!Array.isArray(rawItems)) rawItems = [];
135 rawItems = [item.trim(), ...rawItems];
136 }
137
138
139 if (typeof rawItems === 'string') rawItems = [rawItems];
140
141 if (!Array.isArray(rawItems) || rawItems.length === 0) {
142 await Actor.pushData({ error: 'missing_items', message: 'Input required: provide "items" (array) or "item" (single string). Example: {"items": ["New York, NY"]}' });
143 await Actor.exit('Skipped: Input required: provide "items" (array) or "item" (single string). Example: {"items": ["New York, NY"]}');
144 return;
145 }
146
147 const validStyles = ['short', 'long', 'components'];
148 if (!validStyles.includes(style)) {
149 const message = `Invalid style "${style}". Must be one of: ${validStyles.join(', ')}`;
150 await Actor.pushData({ error: 'invalid_style', message });
151 await Actor.exit(`Skipped: ${message}`);
152 return;
153 }
154
155
156 const items = normalizeItems(rawItems);
157
158 log.info(`Processing ${items.length} location(s) with style "${style}"`);
159
160 const results = [];
161 let processed = 0;
162
163 for (let i = 0; i < items.length; i++) {
164 const original = items[i];
165 const id = i + 1;
166
167 try {
168 const result = preprocess(original, style);
169
170 results.push({
171 id,
172 input: original,
173 output: result.value,
174 city: result.city,
175 stateOrProvince: result.stateOrProvince,
176 country: result.country,
177 confidence: Math.round(result.confidence * 100) / 100,
178 });
179
180 processed++;
181
182 if (processed % 100 === 0) {
183 log.info(`Processed ${processed}/${items.length} locations`);
184 }
185 } catch (err) {
186 log.warning(`Failed to process item ${id}: ${err.message}`);
187 results.push({
188 id,
189 input: original,
190 output: original,
191 city: null,
192 stateOrProvince: null,
193 country: null,
194 confidence: 0,
195 error: err.message,
196 });
197 }
198 }
199
200
201
202 await Actor.pushData(results);
203
204 log.info(`Completed processing ${results.length} location(s)`);
205
206
207 const kvStore = await Actor.openKeyValueStore();
208 await kvStore.setValue('summary', {
209 totalItems: results.length,
210 processedAt: new Date().toISOString(),
211 style,
212 sampleResults: results.slice(0, 5),
213 });
214}
215
216
217
218
219
220function getLandingPageHtml() {
221 return `<!DOCTYPE html>
222<html lang="en">
223<head>
224<meta charset="utf-8">
225<meta name="viewport" content="width=device-width, initial-scale=1">
226<title>Superclean Places — Superlative</title>
227<link rel="preconnect" href="https://fonts.googleapis.com">
228<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
229<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
230<style>
231:root{--color-bg:#0a0e14;--color-surface:#12171f;--color-border:#1e2632;--color-text:#e8eaed;--color-text-muted:#8b95a5;--color-accent:#4ade80;--color-accent-dim:rgba(74,222,128,0.15);--font-display:'Space Mono',monospace;--font-body:'DM Sans',sans-serif}
232*{margin:0;padding:0;box-sizing:border-box}
233body{font-family:var(--font-body);background:var(--color-bg);color:var(--color-text);line-height:1.6;min-height:100vh;overflow-x:hidden}
234a{color:var(--color-accent);text-decoration:none;transition:color .2s ease}
235a:hover{color:#6ee7a0}
236.grid-bg{position:fixed;top:0;left:0;width:100%;height:100%;background-image:linear-gradient(var(--color-border) 1px,transparent 1px),linear-gradient(90deg,var(--color-border) 1px,transparent 1px);background-size:60px 60px;opacity:.3;pointer-events:none;z-index:0}
237.accent-line{position:fixed;top:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent 0%,var(--color-accent) 50%,transparent 100%);animation:shimmer 3s ease-in-out infinite;z-index:10}
238@keyframes shimmer{0%,100%{transform:translateX(-100%)}50%{transform:translateX(100%)}}
239@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
240.container{position:relative;z-index:1;max-width:900px;margin:0 auto;padding:0 24px}
241header{padding:32px 0}
242.logo{font-family:var(--font-display);font-size:14px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text);display:flex;align-items:center;gap:10px;text-decoration:none}
243.logo:hover{color:var(--color-text)}
244.logo-mark{width:28px;height:28px;background:var(--color-accent);display:flex;align-items:center;justify-content:center}
245.logo-mark svg{width:16px;height:16px}
246.hero{padding:80px 0 60px;animation:fadeUp .8s ease-out}
247.hero-label{font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.15em;text-transform:uppercase;color:var(--color-accent);margin-bottom:24px;display:flex;align-items:center;gap:12px}
248.hero-label::before{content:'';width:24px;height:1px;background:var(--color-accent)}
249h1{font-family:var(--font-display);font-size:clamp(28px,5vw,44px);font-weight:700;line-height:1.15;margin-bottom:20px;letter-spacing:-.02em}
250h1 .highlight{color:var(--color-accent)}
251.hero-desc{font-size:18px;color:var(--color-text-muted);max-width:540px;margin-bottom:28px;line-height:1.7}
252.hero-link{font-family:var(--font-display);font-size:13px;letter-spacing:.05em;color:var(--color-accent);display:inline-flex;align-items:center;gap:6px}
253.section{padding:60px 0;border-top:1px solid var(--color-border);animation:fadeUp .8s ease-out .2s backwards}
254.section-label{font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.2em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:24px}
255h2{font-family:var(--font-display);font-size:20px;font-weight:700;margin-bottom:16px;letter-spacing:-.01em}
256p,li{font-size:15px;color:var(--color-text-muted);line-height:1.7}
257li{margin-bottom:6px}
258ul{margin:.5rem 0 1rem 1.25rem}
259strong{color:var(--color-text);font-weight:600}
260pre{background:var(--color-surface);color:var(--color-text);padding:16px 20px;overflow-x:auto;margin:12px 0 16px;font-family:var(--font-display);font-size:13px;line-height:1.6;border:1px solid var(--color-border)}
261code{font-family:var(--font-display);font-size:13px}
262p code,li code,td code{background:var(--color-accent-dim);color:var(--color-accent);padding:2px 6px}
263table{width:100%;border-collapse:collapse;margin:12px 0 16px;font-size:14px}
264th,td{border:1px solid var(--color-border);padding:10px 14px;text-align:left}
265th{background:var(--color-surface);color:var(--color-text-muted);font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.05em;text-transform:uppercase}
266td{color:var(--color-text-muted)}
267.badge{display:inline-block;background:var(--color-accent-dim);color:var(--color-accent);padding:3px 8px;font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.05em}
268.note{font-size:13px;color:var(--color-text-muted);margin-top:8px}
269.product-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-top:24px}
270.product-card{background:var(--color-surface);border:1px solid var(--color-border);padding:24px;transition:all .3s ease;text-decoration:none;color:inherit;display:block}
271.product-card:hover{border-color:var(--color-accent);transform:translateY(-2px)}
272.product-name{font-family:var(--font-display);font-size:14px;font-weight:700;margin-bottom:8px;color:var(--color-text);display:flex;align-items:center;gap:8px}
273.product-name .status{font-size:9px;font-weight:400;padding:3px 6px;background:var(--color-accent-dim);color:var(--color-accent);letter-spacing:.1em}
274.product-desc{font-size:14px;color:var(--color-text-muted);line-height:1.5}
275footer{padding:48px 0;border-top:1px solid var(--color-border)}
276.footer-content{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:24px}
277.footer-text{font-size:13px;color:var(--color-text-muted)}
278.footer-links{display:flex;gap:24px}
279.footer-links a{font-family:var(--font-display);font-size:12px;color:var(--color-text-muted);letter-spacing:.05em}
280.footer-links a:hover{color:var(--color-accent)}
281@media(max-width:600px){.hero{padding:60px 0 40px}.section{padding:40px 0}.footer-content{flex-direction:column;align-items:flex-start}pre{font-size:12px;padding:12px 14px}}
282</style>
283</head>
284<body>
285<div class="grid-bg"></div>
286<div class="accent-line"></div>
287
288<div class="container">
289 <header>
290 <a href="https://apify.com/superlativetech?fpr=8e9l1" class="logo">
291 <div class="logo-mark">
292 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
293 <polyline points="4 17 10 11 4 5"></polyline>
294 <line x1="12" y1="19" x2="20" y2="19"></line>
295 </svg>
296 </div>
297 Superlative
298 </a>
299 </header>
300
301 <section class="hero">
302 <div class="hero-label">Superclean</div>
303 <h1>Superclean <span class="highlight">Places</span></h1>
304 <p class="hero-desc">Instant location parsing and normalization. Extract city, state, and country from messy location strings. Sub-second responses via Standby API.</p>
305 <a href="https://apify.com/superlativetech/superclean-places?fpr=8e9l1" class="hero-link">View on Apify Store →</a>
306 </section>
307
308 <section class="section">
309 <div class="section-label">Features</div>
310 <h2>What this does</h2>
311 <ul>
312 <li><strong>Parses locations</strong> — Extract city, state/province, and country from free-text</li>
313 <li><strong>Normalizes format</strong> — Standardize abbreviations, fix capitalization</li>
314 <li><strong>Multiple styles</strong> — Short, long, or component output formats</li>
315 <li><strong>Batch mode</strong> — Process hundreds of locations via the standard Actor run</li>
316 </ul>
317 </section>
318
319 <section class="section">
320 <div class="section-label">Getting Started</div>
321 <h2>Quick start</h2>
322 <p>Replace <code>YOUR_TOKEN</code> with your Apify API token.</p>
323
324 <p style="margin-top:16px"><strong>Parse a location (short format):</strong></p>
325 <pre>curl "https://superlativetech--superclean-places.apify.actor?token=YOUR_TOKEN&input=san+francisco,+ca"</pre>
326
327 <p><strong>Long format:</strong></p>
328 <pre>curl "https://superlativetech--superclean-places.apify.actor?token=YOUR_TOKEN&input=san+francisco,+ca&style=long"</pre>
329
330 <p><strong>Components only:</strong></p>
331 <pre>curl "https://superlativetech--superclean-places.apify.actor?token=YOUR_TOKEN&input=new+york,+ny,+usa&style=components"</pre>
332 </section>
333
334 <section class="section">
335 <div class="section-label">Reference</div>
336 <h2>Query parameters</h2>
337 <table>
338 <tr><th>Parameter</th><th>Required</th><th>Description</th></tr>
339 <tr><td><code>input</code></td><td>Yes</td><td>Location string to parse</td></tr>
340 <tr><td><code>style</code></td><td>No</td><td>Output format: <code>short</code> (default), <code>long</code>, or <code>components</code></td></tr>
341 <tr><td><code>token</code></td><td>Yes</td><td>Your Apify API token</td></tr>
342 </table>
343 </section>
344
345 <section class="section">
346 <div class="section-label">Response</div>
347 <h2>Response format</h2>
348 <pre>{
349 "id": 1,
350 "input": "san francisco, ca",
351 "output": "San Francisco, CA",
352 "city": "San Francisco",
353 "stateOrProvince": "CA",
354 "country": "US",
355 "confidence": 0.9
356}</pre>
357 </section>
358
359 <section class="section">
360 <div class="section-label">Cost</div>
361 <h2>Pricing</h2>
362 <p>Free to use — you only pay standard Apify platform usage.</p>
363 </section>
364
365 <section class="section">
366 <div class="section-label">Auth</div>
367 <h2>Authentication</h2>
368 <p>Authenticate using either method:</p>
369 <ul>
370 <li>Query parameter: <code>?token=YOUR_APIFY_TOKEN</code></li>
371 <li>Header: <code>Authorization: Bearer YOUR_APIFY_TOKEN</code></li>
372 </ul>
373 <p style="margin-top:8px">Get your token from <a href="https://console.apify.com/settings/integrations">Apify Console → Settings → Integrations</a>.</p>
374 </section>
375
376 <section class="section">
377 <div class="section-label">Superlative</div>
378 <h2>More from Superlative</h2>
379 <div class="product-grid">
380 <a href="https://apify.com/superlativetech/superclean-company-names?fpr=8e9l1" class="product-card">
381 <div class="product-name">Company Names <span class="status">Live</span></div>
382 <p class="product-desc">Normalize company names for CRM and cold email outreach.</p>
383 </a>
384 <a href="https://apify.com/superlativetech/superclean-job-titles?fpr=8e9l1" class="product-card">
385 <div class="product-name">Job Titles <span class="status">Live</span></div>
386 <p class="product-desc">Standardize job titles for outreach sequences.</p>
387 </a>
388 <a href="https://apify.com/superlativetech/superclean-person-names?fpr=8e9l1" class="product-card">
389 <div class="product-name">Person Names <span class="status">Live</span></div>
390 <p class="product-desc">Clean and format person names for personalization.</p>
391 </a>
392 <a href="https://apify.com/superlativetech/superclean-product-names?fpr=8e9l1" class="product-card">
393 <div class="product-name">Product Names <span class="status">Live</span></div>
394 <p class="product-desc">Normalize product and brand names from exports.</p>
395 </a>
396 <a href="https://apify.com/superlativetech/superclean-urls?fpr=8e9l1" class="product-card">
397 <div class="product-name">URLs <span class="status">Live</span></div>
398 <p class="product-desc">Clean and normalize URLs from lead data.</p>
399 </a>
400 <a href="https://apify.com/superlativetech/superclean-phone-numbers?fpr=8e9l1" class="product-card">
401 <div class="product-name">Phone Numbers <span class="status">Live</span></div>
402 <p class="product-desc">Format and validate phone numbers.</p>
403 </a>
404 <a href="https://apify.com/superlativetech/dns-lookup?fpr=8e9l1" class="product-card">
405 <div class="product-name">Supernet DNS Lookup <span class="status">Live</span></div>
406 <p class="product-desc">Look up DNS records for any domain.</p>
407 </a>
408 <a href="https://apify.com/superlativetech/http-api?fpr=8e9l1" class="product-card">
409 <div class="product-name">HTTP API <span class="status">Live</span></div>
410 <p class="product-desc">General-purpose HTTP request utility.</p>
411 </a>
412 </div>
413 </section>
414
415 <footer>
416 <div class="footer-content">
417 <p class="footer-text">© 2026 Superlative</p>
418 <div class="footer-links">
419 <a href="https://apify.com/superlativetech?fpr=8e9l1">Apify Store</a>
420 </div>
421 </div>
422 </footer>
423</div>
424
425</body>
426</html>`;
427}