Skip to content

Public Address Lookup on Apply and Signup Forms

Where this file lives

  • This path (docs/membership/public-address-lookup.md) is inside the rakletv3 repo for review and maintenance.
  • Treat this file as the contract for public address lookup behavior on application and signup pages.

Purpose

Public apply and signup forms can autocomplete a member's address when the form includes the addresses.details field. The visitor types an address, picks a suggested result, and Raklet fills the sibling address fields:

  • addresses.details
  • addresses.city
  • addresses.county
  • addresses.state
  • addresses.postalCode
  • addresses.country

This matches the admin settings experience conceptually, but the public pages do not call Google directly. They use Raklet endpoints so we can validate tenant context, rate-limit traffic, cache repeated lookups, and keep the Google Places API key off the browser.

User-Facing Behavior

  • Lookup is enabled only when the rendered form contains addresses.details.
  • Typing fewer than 3 characters does not send a lookup request.
  • Typing 3 or more characters sends a debounced prediction request.
  • Selecting a prediction fetches place details and fills the address fields that exist on the current form.
  • Manual edits still work after autofill.
  • If lookup fails, the form stays usable and the suggestion menu closes.

Scope

Public pages

  • Apply form: https://{permalink}.raklet.org/Apply locally, https://{permalink}.raklet.com/Apply in production.
  • Signup plan details: https://{permalink}.raklet.org/signup/{planId} locally.
  • Signup group members: https://{permalink}.raklet.org/signup/{planId}/groupmembers locally.

Use .raklet.net only for deployed test environment validation. Do not mix .org UI with .net API or backend hosts.

Code paths

  • Application/Controllers/AddressLookupController.cs
  • Application/Infrastructure/GooglePlacesAddressLookupService.cs
  • Application/Infrastructure/GooglePlacesAddressParser.cs
  • Application/Infrastructure/AddressLookupRateLimiter.cs
  • Application/Infrastructure/PublicAddressLookupRateLimitAttribute.cs
  • Application/Scripts/public-address-lookup.js
  • Application/Views/Apply/Index.cshtml
  • Application/Views/Signup/PlanDetails.cshtml
  • Application/Views/Signup/GroupMembers.cshtml
  • Application/Views/Signup/_FormFieldsPage.cshtml

Architecture

Visitor browser
  |
  | GET /{permalink}/AddressLookup/Predictions?query=...&country=...&sessionToken=...
  v
AddressLookupController
  |
  | validate permalink, rate-limit request, validate minimum query length
  v
GooglePlacesAddressLookupService
  |
  | cache repeated lookups, call Google Places using server-side key
  v
Google Places API

The details flow is the same shape, using GET /{permalink}/AddressLookup/Details?placeId=...&sessionToken=....

Abuse Controls

These controls are part of the feature contract because the pages are anonymous and public.

Control Rule
Tenant scope Blank or invalid permalink requests are rejected. Lookup must be tied to an existing organisation.
Rate limiting Predictions allow 30 requests per 60 seconds per connection IP, permalink, and action. Details allow 20 requests per 60 seconds.
IP source Rate limiting uses the connection IP (Request.UserHostAddress), not client-supplied X-Forwarded-For.
Minimum query length Queries shorter than 3 characters return an empty prediction list and do not call Google.
Server-side key The browser never receives the Google Places API key.
Caching Predictions are cached for 5 minutes by country and normalized query. Details are cached for 1 day by place id.
Result limit Prediction responses are capped to 5 usable results.

Configuration

GooglePlacesAddressLookupService reads the Google Places server key from:

GooglePlacesApiKey

The setting must exist in the Application runtime configuration for the environment under test. If the key is missing or invalid, public forms should still render, but address suggestions will not be available.

Field Rendering Contract

Razor opts a field into lookup by rendering:

data-address-lookup="true"

For signup forms that use prefixed names, the field also renders:

data-address-field-name-prefix="..."

The JavaScript uses the prefix to find fields named like prefix[addresses.city]. Without a prefix, it uses plain names like addresses.city.

Route Contract

Address lookup routes must stay tenant-scoped:

/{permalink}/AddressLookup/Predictions
/{permalink}/AddressLookup/Details

Do not add a non-permalink route for these actions. A global anonymous route would create a cheaper abuse path against the server-side Google Places key.

Testing

Automated tests

Raklet.UnitTests/AddressLookupTests.cs covers:

  • Google details response parsing.
  • City fallback and postal suffix formatting.
  • Autocomplete prediction filtering.
  • Rate limiter window behavior.
  • Blank permalink rejection.
  • X-Forwarded-For spoofing does not change the limiter identity.

Run:

.\scripts\dev\run-local-tests.ps1

For a focused local run after editing only this feature:

.\scripts\dev\build-fast.ps1 Debug -Project Raklet.UnitTests -SkipWarmup

Then run AddressLookupTests from Test Explorer or vstest.console.exe.

Browser QA checklist

Use local .raklet.org pages while developing:

  1. Open an apply or signup page for an organisation whose form includes addresses.details.
  2. Confirm public-address-lookup.js is loaded only when the address field is present.
  3. Type 2 characters and confirm no lookup request is sent.
  4. Type 3 or more characters and confirm a predictions request is sent.
  5. Pick a suggestion and confirm details, city, county, state, postal code, and country are filled where those fields exist.
  6. Change the country field and confirm later predictions include the country filter.
  7. Force a failed lookup response and confirm the page does not crash and the form remains submittable.
  8. Burst predictions until a 429 response and confirm the form remains usable.

If a local permalink returns 404 or does not include address fields, browser QA is blocked by local data. Record the blocker instead of marking autocomplete as passed.

Build Notes

This feature changes compiled Application code and adds old-style .csproj entries. After edits to .cs or .csproj, run:

.\scripts\dev\build-fast.ps1 Debug -Project Application

The public script lives under Application/Scripts, not the Raklet.Backend Grunt bundle. JavaScript edits do not require Grunt, but browser QA still needs the local *.raklet.org page to load the updated script.

Maintenance Rules

  • Keep lookup server-mediated. Do not expose the Google Places key in public JavaScript.
  • Keep endpoint routes permalink-scoped.
  • Do not trust client-provided forwarding headers for public anonymous rate limits unless the proxy chain is explicitly controlled and documented.
  • Update this doc whenever rate limits, field mappings, route shapes, or Google Places parsing rules change.