Smarty (formerly SmartyStreets) does offer a solution for Address Validation and Autocomplete. I know this response is 11 years late, but hopefully it will still help for somebody in the future. I got it to work using the below code. It might not be universal for everybody’s implementation, but it should at least get you started!
This is the custom Javascript snippet I used. Please note that this is referencing my knack form, which has a key of ‘view_90’, so you’ll need to replace that with the view key of your form. In that same vein, the Billing Address field that I implemented this on was referenced as ‘field_51’ in the Knack form’s HTML, so you’ll need to replace that with the proper field name as well.
The only other thing you’ll need to change is adding your own Smarty Embedded Key to the base url in line 42. Once you’ve replaced all those values, this solution should be plug and play (at least at time of writing).
Knack.on('view:render:view_90', async function (event) {
const autocompleteDiv = document.createElement('div');
autocompleteDiv.classList.add('smarty-autocomplete');
autocompleteDiv.hidden = true;
const input = document.getElementById('view_90-field_51_street');
input.insertAdjacentElement('afterend', autocompleteDiv);
input.addEventListener('focus', showDropdown);
input.addEventListener('keyup', () => {
if (input.value == '') {
hideDropdown();
} else {
showDropdown();
}
});
input.addEventListener('blur', () => setTimeout(() => hideAutocomplete(autocompleteDiv), 100));
let suggestArray = [];
let spanArray = [];
let curSuggestions = [];
for (let i = 0; i < 100; i++) {
suggestArray.push(document.createElement('div'));
suggestArray[i].classList.add('smarty-suggestion');
suggestArray[i].setAttribute('data-selected', '');
suggestArray[i].setAttribute('data-index', i);
let tempSpan = document.createElement('span');
tempSpan.classList.add('smarty-span');
spanArray.push(tempSpan);
suggestArray[i].appendChild(tempSpan);
autocompleteDiv.appendChild(suggestArray[i]);
}
positionAutocomplete();
window.addEventListener('resize', positionAutocomplete);
window.addEventListener('scroll', positionAutocomplete, true);
const streetField = document.getElementById('view_90-field_51_street');
streetField.addEventListener("keyup", async (event) => {
if (input.value != '') {
const baseUrl = "https://us-autocomplete-pro.api.smarty.com/lookup?key=<your-embedded-key-here>";
var finalUrl = baseUrl + '&search=' + streetField.value;
try {
const response = await fetch(finalUrl);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
else if (response != null) {
json = await response.json();
curSuggestions = json.suggestions;
emptySpans(spanArray, suggestArray);
fillSpans(spanArray, json.suggestions, suggestArray);
}
} catch (error) {
console.error(error.message);
}
}
});
autocompleteDiv.addEventListener('click', async (e) => {
const suggestion = e.target.closest('.smarty-suggestion');
if (!suggestion) return; // click was not on a suggestion
let curIndex = suggestion.getAttribute('data-index');
if (curSuggestions[curIndex].entries > 1) {
const baseUrl = "https://us-autocomplete-pro.api.smarty.com/lookup?key=184623268557701056";
var finalUrl = baseUrl + '&search=' + streetField.value + '&selected=' + suggestion.getAttribute('data-selected');
try {
const response = await fetch(finalUrl);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
else if (response != null) {
let json = await response.json();
curSuggestions = json.suggestions;
emptySpans(spanArray, suggestArray);
fillSpans(spanArray, json.suggestions, suggestArray);
setTimeout(() => input.focus(), 100)
autocompleteDiv.hidden = false;
}
} catch (error) {
console.error(error.message);
}
}
else {
streetField.value = curSuggestions[curIndex].street_line;
if (curSuggestions[curIndex].secondary != null) {
document.getElementById('view_90-field_51_street2').value = curSuggestions[curIndex].secondary;
}
document.getElementById('view_90-field_51_city').value = curSuggestions[curIndex].city;
document.getElementById('view_90-field_51_state').value = curSuggestions[curIndex].state;
document.getElementById('view_90-field_51_zip').value = curSuggestions[curIndex].zipcode;
}
});
});
function positionAutocomplete() {
const input = document.getElementById('view_90-field_51_street'); // adjust selector
const dropdown = document.querySelector('.smarty-autocomplete');
const rect = input.getBoundingClientRect();
dropdown.style.position = 'fixed';
dropdown.style.top = `${rect.bottom}px`;
dropdown.style.left = `${rect.left}px`;
dropdown.style.width = `${rect.width}px`;
}
function showDropdown() {
const autocompleteDiv = document.querySelector('.smarty-autocomplete');
const input = document.getElementById('view_90-field_51_street');
if (input.value != '') {
autocompleteDiv.hidden = false;
}
}
function hideDropdown() {
const autocompleteDiv = document.querySelector('.smarty-autocomplete');
autocompleteDiv.style.hidden = true;
}
function emptySpans(spanArray, suggestArray) {
for (let i = 0; i < spanArray.length; i++) {
spanArray[i].innerHTML = '';
suggestArray[i].setAttribute('data-selected', '');
}
}
function fillSpans(spanArray, resultArray, suggestArray) {
for (let i = 0; i < resultArray.length; i++) {
spanArray[i].innerHTML = buildSuggestion(resultArray[i]);
suggestArray[i].setAttribute('data-selected', buildSelected(resultArray[i]));
if (resultArray[i].entries != null && resultArray[i].entries == 0) {
}
else {
}
}
}
function buildSuggestion(suggestObj) {
let suggestion = ''
if (suggestObj.street_line != null) {
suggestion += suggestObj.street_line;
}
if (suggestObj.city != null) {
suggestion += ' ' + suggestObj.city;
}
if (suggestObj.state != null) {
suggestion += ' ' + suggestObj.state;
}
if (suggestObj.zipcode != null) {
suggestion += ' ' + suggestObj.zipcode;
}
if (suggestObj.secondary != '' && suggestObj.entries != 0 && suggestObj.entries != 1) {
suggestion += ' ' + suggestObj.secondary + '(+' + suggestObj.entries + ')';
}
else if (suggestObj.entries == 1) {
suggestion = suggestObj.street_line + ' ' + suggestObj.secondary + ' ' + suggestObj.city + ' ' + suggestObj.state + ' ' + suggestObj.zipcode;
}
return suggestion;
}
function buildSelected(suggestObj) {
let selected = suggestObj.street_line;
if (suggestObj.secondary != '' && suggestObj.entries != 0) {
selected += ' ' + suggestObj.secondary + ' (' + suggestObj.entries + ')';
}
return selected + ' ' + suggestObj.city + ' ' + suggestObj.state + ' ' + suggestObj.zipcode;
}
function hideAutocomplete(autocompleteDiv) {
autocompleteDiv.hidden = true;
}
There’s also a little bit of custom CSS that makes it fit the form a bit better and hide the dropdown properly. I’ve included what I used below:
.smarty-autocomplete {
z-index: 1000;
background-color: #fff;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-height: 240px;
overflow-y: auto;
margin: 0;
padding: 0;
}
.smarty-span {
padding-left: 13px;
}
.smarty-suggestion:has(span:empty) {
display: none;
}
.smarty-suggestion:hover {
cursor: pointer;
background: lightgrey;
}
The USPS recently implemented some rate limiting on their address verification that can be limiting to users with high volumes, and they don’t actually offer an Autocomplete API solution, so for auto-filling the form, their publicly available APIs may not work for this particular use case.
The Google Autocomplete solution suffers a bit on accuracy. It occasionally suggests addresses that aren’t actually valid and deliverable, but seem plausible or “close enough” to users, which can lead to incorrect address submissions, missed deliveries, etc … You can find a comparison between the Google and Smarty solutions here.
FYI, I do work for Smarty (formerly SmartyStreets). If anybody finding this thread in the future has any questions, you can get direct support from a real human by using the chat feature on our website, or emailing support@smarty.com.