mapquest-agent-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/examples/conversations/store-locator-with-traffic.md +34 -0
- package/index.js +21 -0
- package/package.json +28 -0
- package/skills/mapquest-directions-routing/SKILL.md +261 -0
- package/skills/mapquest-geocoding-patterns/SKILL.md +243 -0
- package/skills/mapquest-key-security/SKILL.md +183 -0
- package/skills/mapquest-search-ahead/SKILL.md +286 -0
- package/skills/mapquest-static-maps/SKILL.md +242 -0
- package/skills/mapquest-store-locator/SKILL.md +242 -0
- package/skills/mapquest-traffic-data/SKILL.md +253 -0
- package/skills/mapquest-web-integration/SKILL.md +318 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mapquest-geocoding-patterns
|
|
3
|
+
description: Address-to-coordinate and coordinate-to-address conversion using the MapQuest Geocoding API. Covers forward geocoding, reverse geocoding, batch geocoding, quality codes, and response parsing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MapQuest Geocoding Patterns
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The MapQuest Geocoding API converts between addresses and geographic coordinates. Base URL: `https://www.mapquestapi.com/geocoding/v1`
|
|
11
|
+
|
|
12
|
+
Always append `?key=YOUR_API_KEY` to every request. Never hardcode the key — use environment variables (`MAPQUEST_API_KEY` or `VITE_MAPQUEST_API_KEY` for Vite projects).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Geocoding Modes
|
|
17
|
+
|
|
18
|
+
### 1. Forward Geocoding (Address → Lat/Lng)
|
|
19
|
+
|
|
20
|
+
**Endpoint:** `GET /geocoding/v1/address`
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
const response = await fetch(
|
|
24
|
+
`https://www.mapquestapi.com/geocoding/v1/address?key=${apiKey}&location=${encodeURIComponent(address)}`
|
|
25
|
+
);
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
const { lat, lng } = data.results[0].locations[0].latLng;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Always check `geocodeQuality` before trusting the result:**
|
|
31
|
+
|
|
32
|
+
| Quality Code | Meaning | Trust Level |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `POINT` | Rooftop/parcel level | ✅ High |
|
|
35
|
+
| `ADDRESS` | Interpolated street address | ✅ High |
|
|
36
|
+
| `INTERSECTION` | Street intersection | ✅ Medium |
|
|
37
|
+
| `STREET` | Street-level match | ⚠️ Medium |
|
|
38
|
+
| `ZIP` | ZIP code centroid | ⚠️ Low |
|
|
39
|
+
| `CITY` | City centroid | ❌ Low |
|
|
40
|
+
| `COUNTY` | County centroid | ❌ Low |
|
|
41
|
+
| `STATE` | State centroid | ❌ Low |
|
|
42
|
+
| `COUNTRY` | Country centroid | ❌ Very Low |
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// Always validate quality before using coords
|
|
46
|
+
const location = data.results[0].locations[0];
|
|
47
|
+
const ACCEPTABLE_QUALITY = ['POINT', 'ADDRESS', 'INTERSECTION', 'STREET'];
|
|
48
|
+
|
|
49
|
+
if (!ACCEPTABLE_QUALITY.includes(location.geocodeQuality)) {
|
|
50
|
+
// Prompt user to refine their address
|
|
51
|
+
showAddressRefinementPrompt();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Reverse Geocoding (Lat/Lng → Address)
|
|
57
|
+
|
|
58
|
+
**Endpoint:** `GET /geocoding/v1/reverse`
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const response = await fetch(
|
|
62
|
+
`https://www.mapquestapi.com/geocoding/v1/reverse?key=${apiKey}&location=${lat},${lng}`
|
|
63
|
+
);
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
const address = data.results[0].locations[0];
|
|
66
|
+
// address.street, address.adminArea5 (city), address.adminArea3 (state), address.postalCode
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Batch Geocoding (Multiple Addresses)
|
|
70
|
+
|
|
71
|
+
**Endpoint:** `POST /geocoding/v1/batch`
|
|
72
|
+
|
|
73
|
+
Use batch geocoding when converting 2–100 addresses. More efficient than individual requests.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const response = await fetch(
|
|
77
|
+
`https://www.mapquestapi.com/geocoding/v1/batch?key=${apiKey}`,
|
|
78
|
+
{
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
locations: [
|
|
83
|
+
'1600 Pennsylvania Ave NW, Washington, DC',
|
|
84
|
+
'Times Square, New York, NY',
|
|
85
|
+
'Golden Gate Bridge, San Francisco, CA'
|
|
86
|
+
],
|
|
87
|
+
options: { thumbMaps: false } // Set false unless you need thumbnail map images
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
// data.results is an array matching input order
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Batch limits:** Max 100 locations per request. For larger sets, chunk the array.
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
function chunk(arr, size) {
|
|
99
|
+
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
|
|
100
|
+
arr.slice(i * size, i * size + size)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function batchGeocode(addresses) {
|
|
105
|
+
const chunks = chunk(addresses, 100);
|
|
106
|
+
const results = [];
|
|
107
|
+
for (const batch of chunks) {
|
|
108
|
+
const res = await geocodeBatch(batch);
|
|
109
|
+
results.push(...res.results);
|
|
110
|
+
// Add delay between batches to avoid rate limits
|
|
111
|
+
await new Promise(r => setTimeout(r, 200));
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Request Options
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
{
|
|
123
|
+
options: {
|
|
124
|
+
thumbMaps: false, // Skip thumbnail images (saves bandwidth, faster response)
|
|
125
|
+
maxResults: 1, // Limit ambiguous address matches (default: 1)
|
|
126
|
+
ignoreLatLngInput: true, // Always geocode even if lat/lng supplied in location
|
|
127
|
+
boundingBox: { // Bias results to a bounding box
|
|
128
|
+
ul: { lat: 40.9, lng: -74.3 },
|
|
129
|
+
lr: { lat: 40.4, lng: -73.6 }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Response Parsing
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
// Safe response parser with fallbacks
|
|
141
|
+
function parseGeocodingResult(data) {
|
|
142
|
+
if (!data?.results?.[0]?.locations?.[0]) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const loc = data.results[0].locations[0];
|
|
146
|
+
return {
|
|
147
|
+
lat: loc.latLng.lat,
|
|
148
|
+
lng: loc.latLng.lng,
|
|
149
|
+
quality: loc.geocodeQuality,
|
|
150
|
+
qualityCode: loc.geocodeQualityCode,
|
|
151
|
+
street: loc.street,
|
|
152
|
+
city: loc.adminArea5,
|
|
153
|
+
county: loc.adminArea4,
|
|
154
|
+
state: loc.adminArea3,
|
|
155
|
+
country: loc.adminArea1,
|
|
156
|
+
zip: loc.postalCode,
|
|
157
|
+
formatted: [loc.street, loc.adminArea5, loc.adminArea3, loc.postalCode]
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join(', ')
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Common Mistakes to Avoid
|
|
167
|
+
|
|
168
|
+
❌ **Don't** use coordinates from a CITY or ZIP quality result for precise operations like routing or distance calculations — the coords are just the centroid.
|
|
169
|
+
|
|
170
|
+
❌ **Don't** geocode on every keystroke. Debounce or wait for form submission.
|
|
171
|
+
|
|
172
|
+
❌ **Don't** ignore the `info.statuscode` field. `0` = success. Any other value is an error.
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
if (data.info.statuscode !== 0) {
|
|
176
|
+
console.error('Geocoding error:', data.info.messages);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
❌ **Don't** expose your API key in client-side code for production apps. Use a backend proxy.
|
|
181
|
+
|
|
182
|
+
✅ **Do** set `thumbMaps: false` unless you actually need the thumbnail — it speeds up responses.
|
|
183
|
+
|
|
184
|
+
✅ **Do** cache geocoding results for addresses you'll look up repeatedly.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Structured Address Input (More Accurate)
|
|
189
|
+
|
|
190
|
+
Instead of a raw string, pass a structured location object for higher accuracy:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
const response = await fetch(
|
|
194
|
+
`https://www.mapquestapi.com/geocoding/v1/address?key=${apiKey}`,
|
|
195
|
+
{
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'Content-Type': 'application/json' },
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
location: {
|
|
200
|
+
street: '1090 N Charlotte St',
|
|
201
|
+
city: 'Lancaster',
|
|
202
|
+
state: 'PA',
|
|
203
|
+
postalCode: '17603',
|
|
204
|
+
country: 'US'
|
|
205
|
+
},
|
|
206
|
+
options: { thumbMaps: false }
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Error Handling Template
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
async function geocodeAddress(address, apiKey) {
|
|
218
|
+
try {
|
|
219
|
+
const url = `https://www.mapquestapi.com/geocoding/v1/address?key=${apiKey}&location=${encodeURIComponent(address)}&thumbMaps=false`;
|
|
220
|
+
const response = await fetch(url);
|
|
221
|
+
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data = await response.json();
|
|
227
|
+
|
|
228
|
+
if (data.info.statuscode !== 0) {
|
|
229
|
+
throw new Error(`MapQuest error: ${data.info.messages.join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const result = parseGeocodingResult(data);
|
|
233
|
+
if (!result) {
|
|
234
|
+
throw new Error('No results found for this address');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return result;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Geocoding failed:', error);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mapquest-key-security
|
|
3
|
+
description: Best practices for securing MapQuest API keys. Covers environment variables, referrer restrictions, server-side proxying, key rotation, and incident response when a key is exposed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MapQuest API Key Security
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Your MapQuest API key is a secret credential. Exposing it allows others to use your quota, incur charges on your account, and potentially abuse MapQuest services. Always treat API keys like passwords.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## The Golden Rules
|
|
15
|
+
|
|
16
|
+
1. **Never hardcode an API key in source code**
|
|
17
|
+
2. **Never commit a key to version control**
|
|
18
|
+
3. **Use referrer restrictions in the MapQuest developer portal**
|
|
19
|
+
4. **For server-rendered or backend apps: proxy all MapQuest requests through your server**
|
|
20
|
+
5. **Rotate keys if you suspect exposure**
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Environment Variable Storage
|
|
25
|
+
|
|
26
|
+
### Frontend (Vite / React / Vue)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# .env.local (never commit this file)
|
|
30
|
+
VITE_MAPQUEST_API_KEY=your_key_here
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
// Access in code:
|
|
35
|
+
const apiKey = import.meta.env.VITE_MAPQUEST_API_KEY;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
# .gitignore — ensure .env files are excluded
|
|
40
|
+
.env
|
|
41
|
+
.env.local
|
|
42
|
+
.env*.local
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Backend (Node.js)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# .env
|
|
49
|
+
MAPQUEST_API_KEY=your_key_here
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
require('dotenv').config();
|
|
54
|
+
const apiKey = process.env.MAPQUEST_API_KEY;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Never do this:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// ❌ WRONG — key is visible in source code and bundle
|
|
61
|
+
const apiKey = 'Ab1Cd2Ef3Gh4Ij5Kl6Mn7Op8Qr9';
|
|
62
|
+
|
|
63
|
+
// ❌ WRONG — key in git history forever
|
|
64
|
+
// git commit -m "added api key"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Referrer Restrictions (Developer Portal)
|
|
70
|
+
|
|
71
|
+
In the [MapQuest Developer Portal](https://developer.mapquest.com), set **Allowed Referrers** for each key:
|
|
72
|
+
|
|
73
|
+
- `https://yourdomain.com/*` — production
|
|
74
|
+
- `https://staging.yourdomain.com/*` — staging
|
|
75
|
+
- `http://localhost:*` — local development
|
|
76
|
+
|
|
77
|
+
This limits the key to only work when requests originate from your domains. Client-side keys with referrer restrictions are significantly safer than unrestricted keys.
|
|
78
|
+
|
|
79
|
+
**Limitation:** Referrer headers can be spoofed by determined bad actors. For truly sensitive apps, use server-side proxying instead.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Server-Side Proxy (Recommended for Production)
|
|
84
|
+
|
|
85
|
+
Never send MapQuest API requests directly from client-side code in production. Instead, proxy through your backend:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// Express.js proxy route
|
|
89
|
+
const express = require('express');
|
|
90
|
+
const router = express.Router();
|
|
91
|
+
|
|
92
|
+
router.get('/geocode', async (req, res) => {
|
|
93
|
+
const { address } = req.query;
|
|
94
|
+
|
|
95
|
+
// Validate input
|
|
96
|
+
if (!address || address.length > 500) {
|
|
97
|
+
return res.status(400).json({ error: 'Invalid address' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Optional: rate limit per user/IP here
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(
|
|
104
|
+
`https://www.mapquestapi.com/geocoding/v1/address?key=${process.env.MAPQUEST_API_KEY}&location=${encodeURIComponent(address)}&thumbMaps=false`
|
|
105
|
+
);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
res.json(data);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
res.status(500).json({ error: 'Geocoding service unavailable' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
// Client-side — call your proxy, not MapQuest directly
|
|
116
|
+
const response = await fetch(`/api/geocode?address=${encodeURIComponent(address)}`);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Benefits of proxying:**
|
|
120
|
+
- API key never exposed to browser
|
|
121
|
+
- You can rate-limit per user
|
|
122
|
+
- You can cache responses
|
|
123
|
+
- You can log and monitor usage
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Key Rotation Strategy
|
|
128
|
+
|
|
129
|
+
Create multiple keys in the MapQuest Developer Portal:
|
|
130
|
+
|
|
131
|
+
| Key Name | Purpose | Restrictions |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `production-web` | Live site | Referrer: `https://yourdomain.com/*` |
|
|
134
|
+
| `staging-web` | Staging environment | Referrer: `https://staging.yourdomain.com/*` |
|
|
135
|
+
| `development` | Local dev | Referrer: `http://localhost:*` |
|
|
136
|
+
| `server-side` | Backend proxy | No referrer (IP-restricted if possible) |
|
|
137
|
+
|
|
138
|
+
Rotate keys every 90 days or immediately after any potential exposure.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Incident Response: Exposed Key
|
|
143
|
+
|
|
144
|
+
If you suspect a key was exposed (e.g., committed to a public repo):
|
|
145
|
+
|
|
146
|
+
1. **Immediately regenerate/revoke the key** in the MapQuest Developer Portal
|
|
147
|
+
2. **Deploy a new key** from your secure environment variable store
|
|
148
|
+
3. **Purge git history** if committed (use `git filter-branch` or BFG Repo Cleaner)
|
|
149
|
+
4. **Check MapQuest usage logs** for unusual activity
|
|
150
|
+
5. **Notify your team** and update deployment pipelines
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# If key was committed — remove from git history
|
|
154
|
+
# Install BFG: https://rtyley.github.io/bfg-repo-cleaner/
|
|
155
|
+
bfg --replace-text secrets.txt my-repo.git
|
|
156
|
+
git push --force
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Checklist for Every Project
|
|
162
|
+
|
|
163
|
+
- [ ] API key stored in `.env` / environment variable, never in source code
|
|
164
|
+
- [ ] `.env` files added to `.gitignore`
|
|
165
|
+
- [ ] Referrer restrictions configured in MapQuest Developer Portal
|
|
166
|
+
- [ ] Separate keys for production, staging, and development
|
|
167
|
+
- [ ] Server-side proxy implemented for production (recommended)
|
|
168
|
+
- [ ] Key rotation schedule documented (every 90 days)
|
|
169
|
+
- [ ] Team members aware not to log or share keys in chat/tickets
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## What Not to Do
|
|
174
|
+
|
|
175
|
+
❌ Don't log API keys in console.log or server logs
|
|
176
|
+
|
|
177
|
+
❌ Don't share API keys in Slack, email, or issue trackers
|
|
178
|
+
|
|
179
|
+
❌ Don't use the same key for development and production
|
|
180
|
+
|
|
181
|
+
❌ Don't store keys in local storage or cookies (accessible to JS/XSS)
|
|
182
|
+
|
|
183
|
+
❌ Don't put keys in URLs (they appear in server logs and browser history)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mapquest-search-ahead
|
|
3
|
+
description: Typeahead and autocomplete using the MapQuest Search Ahead API. Covers debouncing, result categories, geographic bias, collection types, and chaining with the Geocoding API.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MapQuest Search Ahead Patterns
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The Search Ahead API powers autocomplete/typeahead address and POI search. Base URL: `https://www.mapquestapi.com/search/v3/prediction`
|
|
11
|
+
|
|
12
|
+
Use this for live search inputs, not for batch geocoding or one-shot address resolution. For a single definitive address lookup, use the Geocoding API.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Basic Search Ahead Request
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const response = await fetch(
|
|
20
|
+
`https://www.mapquestapi.com/search/v3/prediction?key=${apiKey}&q=${encodeURIComponent(query)}&collection=address,adminArea,poi`
|
|
21
|
+
);
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
// data.results is an array of prediction objects
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Collection Types
|
|
29
|
+
|
|
30
|
+
The `collection` parameter controls what types of results are returned. Always specify what you need — don't request everything.
|
|
31
|
+
|
|
32
|
+
| Collection | Description | Use When |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `address` | Street addresses | Address entry forms |
|
|
35
|
+
| `adminArea` | Cities, states, countries | Location pickers |
|
|
36
|
+
| `airport` | Airport names + IATA codes | Travel apps |
|
|
37
|
+
| `category` | Category labels (e.g., "restaurants") | Search-by-type UIs |
|
|
38
|
+
| `franchise` | Chain business names (e.g., "Starbucks") | Store locators |
|
|
39
|
+
| `poi` | Individual points of interest | General search |
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// Address-only (fastest, most relevant for delivery/shipping forms)
|
|
43
|
+
collection=address
|
|
44
|
+
|
|
45
|
+
// Address + city for flexible location search
|
|
46
|
+
collection=address,adminArea
|
|
47
|
+
|
|
48
|
+
// Full search (POI + address + city)
|
|
49
|
+
collection=poi,address,adminArea
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Geographic Bias
|
|
55
|
+
|
|
56
|
+
Bias results toward a location to get more relevant suggestions:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
const params = new URLSearchParams({
|
|
60
|
+
key: apiKey,
|
|
61
|
+
q: query,
|
|
62
|
+
collection: 'address,poi',
|
|
63
|
+
location: `${userLng},${userLat}`, // NOTE: lng,lat order (not lat,lng!)
|
|
64
|
+
limit: 5,
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Critical:** The `location` parameter uses **longitude, latitude** order (opposite of most MapQuest parameters).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Debouncing — Required
|
|
73
|
+
|
|
74
|
+
Always debounce Search Ahead requests. Fire no more than once per 300–500ms. Never call the API on every keystroke.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// Vanilla JS debounce
|
|
78
|
+
function debounce(fn, delay) {
|
|
79
|
+
let timer;
|
|
80
|
+
return (...args) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const searchInput = document.getElementById('search');
|
|
87
|
+
const resultsContainer = document.getElementById('results');
|
|
88
|
+
|
|
89
|
+
const handleSearch = debounce(async (query) => {
|
|
90
|
+
if (query.length < 2) {
|
|
91
|
+
resultsContainer.innerHTML = '';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = await searchAhead(query);
|
|
96
|
+
renderResults(data.results);
|
|
97
|
+
}, 300);
|
|
98
|
+
|
|
99
|
+
searchInput.addEventListener('input', e => handleSearch(e.target.value));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// React hook with debounce
|
|
104
|
+
import { useState, useEffect } from 'react';
|
|
105
|
+
|
|
106
|
+
function useSearchAhead(apiKey, debounceMs = 300) {
|
|
107
|
+
const [query, setQuery] = useState('');
|
|
108
|
+
const [results, setResults] = useState([]);
|
|
109
|
+
const [loading, setLoading] = useState(false);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (query.length < 2) {
|
|
113
|
+
setResults([]);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const timer = setTimeout(async () => {
|
|
118
|
+
setLoading(true);
|
|
119
|
+
try {
|
|
120
|
+
const data = await fetchPredictions(query, apiKey);
|
|
121
|
+
setResults(data.results || []);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error(e);
|
|
124
|
+
} finally {
|
|
125
|
+
setLoading(false);
|
|
126
|
+
}
|
|
127
|
+
}, debounceMs);
|
|
128
|
+
|
|
129
|
+
return () => clearTimeout(timer);
|
|
130
|
+
}, [query, apiKey, debounceMs]);
|
|
131
|
+
|
|
132
|
+
return { query, setQuery, results, loading };
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Parsing Results
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
function parsePrediction(result) {
|
|
142
|
+
return {
|
|
143
|
+
id: result.id,
|
|
144
|
+
displayString: result.displayString, // Human-readable label
|
|
145
|
+
place: result.place, // Detailed place info
|
|
146
|
+
collection: result.collection, // 'address', 'poi', etc.
|
|
147
|
+
// For addresses:
|
|
148
|
+
street: result.place?.properties?.street,
|
|
149
|
+
city: result.place?.properties?.city,
|
|
150
|
+
state: result.place?.properties?.stateCode,
|
|
151
|
+
zip: result.place?.properties?.postalCode,
|
|
152
|
+
country: result.place?.properties?.countryCode,
|
|
153
|
+
// Coordinates (if available in result):
|
|
154
|
+
lat: result.place?.geometry?.coordinates?.[1],
|
|
155
|
+
lng: result.place?.geometry?.coordinates?.[0],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Chaining Search Ahead → Geocoding
|
|
163
|
+
|
|
164
|
+
Search Ahead results often don't include precise lat/lng coordinates. Chain with the Geocoding API to resolve:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
async function selectPrediction(prediction, apiKey) {
|
|
168
|
+
// Some predictions include coordinates directly
|
|
169
|
+
if (prediction.place?.geometry?.coordinates) {
|
|
170
|
+
const [lng, lat] = prediction.place.geometry.coordinates;
|
|
171
|
+
return { lat, lng, label: prediction.displayString };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Otherwise, geocode the display string
|
|
175
|
+
const geoResult = await geocodeAddress(prediction.displayString, apiKey);
|
|
176
|
+
return {
|
|
177
|
+
lat: geoResult.lat,
|
|
178
|
+
lng: geoResult.lng,
|
|
179
|
+
label: prediction.displayString,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Full Autocomplete Component (Vanilla JS)
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
class MapQuestAutocomplete {
|
|
190
|
+
constructor(inputEl, apiKey, onSelect) {
|
|
191
|
+
this.input = inputEl;
|
|
192
|
+
this.apiKey = apiKey;
|
|
193
|
+
this.onSelect = onSelect;
|
|
194
|
+
this.dropdown = this.createDropdown();
|
|
195
|
+
this.debounceTimer = null;
|
|
196
|
+
|
|
197
|
+
this.input.addEventListener('input', () => this.handleInput());
|
|
198
|
+
this.input.addEventListener('blur', () => setTimeout(() => this.hide(), 200));
|
|
199
|
+
document.addEventListener('click', e => {
|
|
200
|
+
if (!this.input.contains(e.target)) this.hide();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
createDropdown() {
|
|
205
|
+
const el = document.createElement('ul');
|
|
206
|
+
el.style.cssText = 'position:absolute;background:#fff;border:1px solid #ccc;list-style:none;margin:0;padding:0;width:100%;z-index:1000;display:none';
|
|
207
|
+
this.input.parentElement.style.position = 'relative';
|
|
208
|
+
this.input.parentElement.appendChild(el);
|
|
209
|
+
return el;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
handleInput() {
|
|
213
|
+
clearTimeout(this.debounceTimer);
|
|
214
|
+
const q = this.input.value.trim();
|
|
215
|
+
if (q.length < 2) { this.hide(); return; }
|
|
216
|
+
this.debounceTimer = setTimeout(() => this.search(q), 300);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async search(q) {
|
|
220
|
+
const url = `https://www.mapquestapi.com/search/v3/prediction?key=${this.apiKey}&q=${encodeURIComponent(q)}&collection=address,adminArea,poi&limit=5`;
|
|
221
|
+
const res = await fetch(url);
|
|
222
|
+
const data = await res.json();
|
|
223
|
+
this.show(data.results || []);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
show(results) {
|
|
227
|
+
this.dropdown.innerHTML = '';
|
|
228
|
+
results.forEach(r => {
|
|
229
|
+
const li = document.createElement('li');
|
|
230
|
+
li.textContent = r.displayString;
|
|
231
|
+
li.style.cssText = 'padding:8px 12px;cursor:pointer';
|
|
232
|
+
li.addEventListener('mouseover', () => li.style.background = '#f0f0f0');
|
|
233
|
+
li.addEventListener('mouseout', () => li.style.background = '');
|
|
234
|
+
li.addEventListener('click', () => {
|
|
235
|
+
this.input.value = r.displayString;
|
|
236
|
+
this.hide();
|
|
237
|
+
this.onSelect(r);
|
|
238
|
+
});
|
|
239
|
+
this.dropdown.appendChild(li);
|
|
240
|
+
});
|
|
241
|
+
this.dropdown.style.display = results.length ? 'block' : 'none';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
hide() { this.dropdown.style.display = 'none'; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Usage:
|
|
248
|
+
const autocomplete = new MapQuestAutocomplete(
|
|
249
|
+
document.getElementById('address-input'),
|
|
250
|
+
'YOUR_API_KEY',
|
|
251
|
+
async (prediction) => {
|
|
252
|
+
const location = await selectPrediction(prediction, 'YOUR_API_KEY');
|
|
253
|
+
map.setCenter([location.lat, location.lng]);
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Common Mistakes to Avoid
|
|
261
|
+
|
|
262
|
+
❌ **Don't** fire Search Ahead on every keypress — always debounce.
|
|
263
|
+
|
|
264
|
+
❌ **Don't** set `limit` higher than needed (max 10). Larger result sets are slower and rarely improve UX.
|
|
265
|
+
|
|
266
|
+
❌ **Don't** confuse `location` parameter order. Search Ahead uses **lng,lat**. Geocoding API uses **lat,lng**. Always double-check.
|
|
267
|
+
|
|
268
|
+
❌ **Don't** assume Search Ahead results include coordinates. They may not — always check before using and fall back to geocoding.
|
|
269
|
+
|
|
270
|
+
✅ **Do** set a minimum character threshold (2–3 chars) before calling the API.
|
|
271
|
+
|
|
272
|
+
✅ **Do** cancel in-flight requests when a newer query supersedes them (use AbortController).
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
let controller;
|
|
276
|
+
|
|
277
|
+
async function fetchPredictions(q, apiKey) {
|
|
278
|
+
if (controller) controller.abort();
|
|
279
|
+
controller = new AbortController();
|
|
280
|
+
|
|
281
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
282
|
+
return res.json();
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
✅ **Do** handle network errors gracefully — the dropdown should fail silently, not crash the page.
|