AssociationAI / AI Literacy
Trihelix AI team Published

Tutorial

Intermediate

Put Your Chapters on a Map Without Pinning a Single Member

Build a static chapter map with free software, publish counts instead of people, and know exactly what leaves a visitor's browser.

Time needed: About 45 minutes

Before you start:

  • A list of your chapters with a city or office address and a member count for each (counts only, never member rows)
  • A spreadsheet program, a text editor and any recent desktop browser
  • Somewhere to host static files, or just your own computer to try it

Mapping Chapters Member Data Privacy Openstreetmap

By the end of this tutorial you will have a static web page that draws your chapters as circles sized by member count, hides chapters too small to show safely, and holds no member rows at all.

Try it now: the chapter map lab runs the build in your browser. Paste chapter counts, choose a minimum, and download the file you would publish. Your table is read in that tab and goes nowhere.

Why the map shows chapters and never members

A dot for every member is a published address list. NIST’s guidance on de-identifying government datasets notes that some locations are highly identifying, and its example is a house where one person lives. It also says shielding an identity can take about 100 meters of added noise in an urban area but 5 kilometers or more in a rural one. We read this as a reason to publish counts per chapter: how far a member dot must move to be safe depends on where that member lives.

The FTC’s December 2024 action over location data rests on a complaint saying the data two companies sold can identify consumers and is not anonymized. For how a small chapter singles out one member, see Check Member Data Before You Paste It Into an AI Tool. We found no published study showing that a member map raises engagement, so treat it as a way to see your chapters, not proof that anything works.

Make the map in seven steps

  1. Write one row per chapter. In a spreadsheet, make five columns: chapter, state, lat, lon and members. The members column holds a count, never a list.
  2. Look up each chapter’s coordinates from a public address. Use the chapter’s office or regular meeting place, never a member’s address. The Census Geocoder returns the longitude as x and the latitude as y, and it covers only the United States, Puerto Rico and the U.S. Island Areas.
  3. Choose a minimum count. Decide the smallest chapter you will show. Five is a working choice, not a figure from any source, so let your privacy lead set yours.
  4. Draw it in the lab. Paste the table into the lab, set the minimum and press Draw the map. Download chapters.geojson once the status line says it matches the map.

Diagram: chapters.csv stays on your machine, the lab hides chapters below your minimum, and only chapters.geojson is published; names, email addresses, street addresses and member numbers appear in neither file.

  1. Put the page and its files in one folder. The starter kit holds index.html, app.js and the Leaflet files; add your chapters.geojson.
  2. Serve the folder. In a terminal there, run python3 -m http.server 8000 and open 127.0.0.1:8000 in your browser, or upload the folder to any static host. A page opened by double-click cannot read its data files.

Diagram: index.html, app.js, chapters.geojson, states.geojson and the Leaflet files sit in one folder, and the visitor's browser draws the map from them without contacting any other website.

  1. Decide on the background. The page uses a plain background by default; to add the OpenStreetMap basemap, change data-basemap="none" to data-basemap="osm" in index.html.

Diagram: with a plain background only your site serves the visitor, while the OpenStreetMap basemap adds a request to the tile servers, which see the visitor's IP address and which site asked.

The OpenStreetMap Foundation’s tile policy says the map data is free for everyone to use, yet availability is best effort. It requires visible attribution, bars pages from setting a restrictive Referrer-Policy that stops the Referer header being sent, and bars saving tiles for offline use. Chapters outside the United States need another route for coordinates. The foundation’s address search policy sets an absolute maximum of one request per second, requires cached results for bulk lookups, and bans search-as-you-type on its public service. It also says language models may suggest the service only if they point prominently to the policy.

The two files that draw the map

These are the starter kit files exactly as we ran them. Against a local server in desktop Chromium, the page requested only files from its own folder. The script refuses to draw if the chapters file has a field beyond the four it expects, which catches an email column added by mistake.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chapter map</title>
<link rel="stylesheet" href="leaflet.css">
<style>
  body { margin: 0; font: 16px/1.5 Helvetica, Arial, sans-serif; background: #ffffff; color: #1b2333; }
  main { max-width: 960px; margin: 0 auto; padding: 1rem; }
  #map { height: 60vh; min-height: 340px; border: 1px solid #33415c; background: #eef3fb; }
  #note { color: #55607a; }
  table { border-collapse: collapse; width: 100%; }
  th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid #b9c4da; }
</style>
</head>
<body>
<main>
  <h1>Chapter map</h1>
  <p id="note" role="status">Loading the map.</p>
  <div id="map" data-basemap="none" role="img" aria-label="Map of chapters. A table with the same information follows."></div>
  <h2>Chapters</h2>
  <table>
    <thead><tr><th>Chapter</th><th>State</th><th>Members</th></tr></thead>
    <tbody id="chapter-rows"></tbody>
  </table>
</main>
<script src="leaflet.js"></script>
<script src="app.js"></script>
</body>
</html>
// Draws states.geojson and chapters.geojson. Those two files are the only things it fetches.
(function () {
  'use strict';
  var ALLOWED = ['chapter', 'state', 'members', 'suppressed'];
  var mapEl = document.getElementById('map');
  var note = document.getElementById('note');
  var rows = document.getElementById('chapter-rows');

  function getJSON(url) {
    return fetch(url).then(function (r) {
      if (!r.ok) throw new Error(url + ' returned status ' + r.status);
      return r.json();
    });
  }

  function checkChapters(data) {
    if (!data || data.type !== 'FeatureCollection' || !Array.isArray(data.features)) {
      throw new Error('chapters.geojson is not a FeatureCollection.');
    }
    data.features.forEach(function (f) {
      var p = f.properties || {};
      Object.keys(p).forEach(function (key) {
        if (ALLOWED.indexOf(key) === -1) {
          throw new Error('chapters.geojson has a field named "' + key + '". Remove it before publishing.');
        }
      });
      var c = f.geometry && f.geometry.coordinates;
      if (!f.geometry || f.geometry.type !== 'Point' || !c || !isFinite(c[0]) || !isFinite(c[1])) {
        throw new Error('Every chapter must be a single Point with a latitude and longitude.');
      }
    });
    return data;
  }

  function membersText(p) {
    return p.suppressed || p.members === null ? 'Hidden (small chapter)' : String(p.members);
  }

  var map = L.map(mapEl).setView([39.5, -98.35], 4);

  if (mapEl.getAttribute('data-basemap') === 'osm') {
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);
  }

  Promise.all([getJSON('states.geojson'), getJSON('chapters.geojson')]).then(function (d) {
    var chapters = checkChapters(d[1]);
    L.geoJSON(d[0], { style: { color: '#33415c', weight: 1, fillColor: '#c9d6ec', fillOpacity: 0.6 } }).addTo(map);
    L.geoJSON(chapters, {
      pointToLayer: function (f, latlng) {
        var n = f.properties.members;
        var radius = n === null ? 6 : Math.max(8, Math.sqrt(n) * 1.6);
        return L.circleMarker(latlng, {
          radius: radius, weight: 2, color: '#1b2333',
          fillColor: n === null ? '#8a94a8' : '#2e6fd0', fillOpacity: 0.85
        });
      },
      onEachFeature: function (f, layer) {
        var box = document.createElement('div');
        var name = document.createElement('strong');
        name.textContent = f.properties.chapter;
        box.appendChild(name);
        box.appendChild(document.createElement('br'));
        box.appendChild(document.createTextNode(membersText(f.properties)));
        layer.bindPopup(box);

        var tr = document.createElement('tr');
        [f.properties.chapter, f.properties.state, membersText(f.properties)].forEach(function (text) {
          var td = document.createElement('td');
          td.textContent = text;
          tr.appendChild(td);
        });
        rows.appendChild(tr);
      }
    }).addTo(map);
    var hidden = chapters.features.filter(function (f) { return f.properties.suppressed; }).length;
    note.textContent = 'Showing ' + chapters.features.length + ' chapters; ' + hidden + ' with the count hidden.';
  }).catch(function (err) {
    note.textContent = location.protocol === 'file:'
      ? 'Browsers will not read the data files from a page opened by double-click. Serve this folder (see README.md).'
      : 'The map did not draw: ' + err.message;
  });
})();

A teaching example: eight chapters, one hidden

This is a teaching example, not a case study. A fictional association has eight U.S. chapters, and the smallest, in Anchorage, falls under the minimum. The lab publishes that circle in grey with no count; the other seven keep theirs.

The association plans a Chicago meeting and draws a 500-mile ring around it. Only the Chicago chapter sits inside. Widen the ring to 650 miles and Atlanta and National Capital join, at about 589 and 594 miles.

Check the result

  • Reload the page with the network panel open. In plain mode it should request only files from your own folder (a browser may also add a favicon request).
  • Open chapters.geojson in a text editor: each chapter should have only four fields, and chapters under your minimum should show a null count.
  • Add a field named email to one chapter in a copy of the file. The page should refuse to draw and name the field.
  • Compare the table under the map with the file; both should list the same chapters and counts.

Four mistakes that make a chapter map leak or mislead

Geocoding the member list to save time sends it to a stranger. The foundation’s tile policy and its address search policy both tell users not to submit personal data, and our word search of the Census Geocoder documentation found no mention of privacy, retention or storage.

Publishing a total beside the map hands readers a subtraction: remove the visible chapters from the total and the hidden count comes back. Leave totals off.

A basemap tells a third party about your page, and the foundation publishes summary data on the websites that use its tiles, so say so in your privacy notice.

A ring measures straight-line distance, so reading it as a drive time overstates who can attend.

What the chapter map kit contains

The kit holds the finished page, the example chapter table and its published file, state outlines built from the Census Bureau’s 2025 state file at 1:20,000,000 (a boundary file the bureau designs for small scale thematic mapping), a copy of Leaflet with its open-source license, and a pre-publish checklist. The download is free once you add your email to the form on the demo page.

Sources

  • OpenStreetMap Foundation, Tile Usage Policy: availability, attribution, referrer, offline and usage-data rules.
  • OpenStreetMap Foundation, Nominatim Usage Policy: request limit, caching, search-as-you-type, personal data and language models.
  • U.S. Census Bureau, Census Geocoder documentation: coverage area and coordinate output.
  • U.S. Census Bureau, Cartographic Boundary Files: outlines for small scale thematic mapping.
  • NIST SP 800-188: highly identifying locations and the noise needed to shield them.
  • FTC press release, December 3, 2024: location data that is not anonymized.
Sources (6)