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