FEMA National Flood Hazard Layer (NFHL)

FEMA maintains a list of downloadable NFHL shapefiles for thousands of U.S. counties and communities. However, it can be difficult to get just the data you need. Perhaps you want data for a specific spatial region, by date, FIRMID, community ID or FIPS. This GDAL pipeline one-liner can be run nightly to maintain an up-to-date queryable data base in GeoPackage (GPKG) format. It takes about 5 minutes run, resulting in a GeoPackage file named nfhl.gpkg.

Now that you've added the script to cron and run it once, let's see what we can do with the nfhl.gpkg. The following attributes are available:

Let's talk about the NFHL shapefiles, briefly. There are over 2,600 and growing. Each file has multiple layers, they are:

S_BASE_INDEX, S_BFE, S_CST_GAGE, S_CST_TSCT_LN, S_DATUM_CONV_PT, S_FIRM_PAN, S_FLD_HAZ_AR, S_FLD_HAZ_LN, S_GEN_STRUCT, S_LABEL_LD, S_LABEL_PT, S_LIMWA, S_LOMR, S_PFD_LN, S_PLSS_AR, S_POL_AR, S_PROFIL_BASLN, S_STN_START, S_SUBBASINS, S_SUBMITTAL_INFO, S_TSCT_BASLN, S_WTR_AR, S_WTR_LN, S_XS, L_COMM_INFO, L_COMM_REVIS, L_CST_MODEL, L_CST_TSCT_ELEV, L_MANNINGSN, L_MEETINGS, L_MT2_LOMR, L_MTG_POC, L_PAN_REVIS, L_SOURCE_CIT, L_SUMMARY_DISCHARGES, L_XS_ELEV, STUDY_INFO

What each of those layers have in common is a DFIRM_ID attribute. This identifies its unique area. Typically, the DFIRM_ID is the state/county FIPS, with a 'C' appended. Example: 06015C which is Del Norte County, CA. In some cases the DFIRM_ID will not have that pattern, it will be a community id, like 230025, which is the Town of Mapleton.

The key to remember is, DFIRM_ID is how you associate all the layers.

Let's try a few examples extracting meta-data from nfhl.gpkg, building a query and finally downloading/aggregating our request direct from FEMA into a single file.

NFHL county/community data for all of Florida updated within the last 7 days
# Get NFHL data for Florida, using state FIPS 12, last 7 days, save as a GeoPackage
gdal vector concat --mode merge-per-layer-name \
   $(gdal vector sql --dialect sqlite --sql "select vsiurl from nfhl where stcofips like '12%' and updated between 20260505 and 20260512" -i nfhl.gpkg --lco HEADER=NO --of CSV -o /vsistdout/) \
   florida_$(date +%Y%m%d).gpkg --overwrite
NFHL county/community data for all of Rhode Island (or any state)
Rhode Island state FIPS is 44. Build the query, download all shape files and save as a single GeoPackage, with all the above layers:

gdal vector pipeline \
   ! concat --mode merge-per-layer-name \
     $(gdal vector sql -i nfhl.gpkg -o /vsistdout/ --dialect sqlite --sql "select group_concat(vsiurl, ' ') from nfhl where stcofips like '44%'" --lco HEADER=NO --of CSV) \
   ! write -o rhodeIsland_20260512.gpkg --overwrite
Single County Download by State/County FIPS
# Get NFHL data for Charlotte County Florida, using state/county FIPS 12015, save as a GeoPackage
gdal vector convert \
   -i $(gdal vector sql --dialect sqlite --sql "select vsiurl from nfhl where stcofips = '12015'" -i nfhl.gpkg --lco HEADER=NO --of CSV -o /vsistdout/) \
   -o charlotte.gpkg --overwrite
Getting all DFIRM_ID's, community ids, locales and data url's for a State/County FIPS '08117', which is Summit County, Colorado
gdal vector sql \
   -i nfhl.gpkg --dialect sqlite \
   --sql "select \
      group_concat(dfirmid, ',') as dfirmids, group_concat(cids, ', ') as cids, group_concat(locale,', ') as locales, group_concat(url, ' ') as urls \
      from nfhl where stcofips = '08117'" \
   -o /vsistdout/  --of CSV --lco STRING_QUOTING=ALWAYS

Which returns the CSV:

"dfirmids","cids","locales","urls"
"08117C","080016,080072,080172,080201,080237,080245,080290","SUMMIT COUNTY","https://hazards.fema.gov/femaportal/NFHL/Download/ProductsDownLoadServlet?DFIRMID=0&state=s&county=c&fileName=08117C_20240512.zip"

Alternatively, you could change where stcofips = '08117' to where stcofips like '08%' to get all the NFHL county/community data for the entire state of Colorado.
GDAL Pipeline One-Liner
#!/usr/bin/env bash

# Download FEMA polygons for all county and community level data, cl2024_v1.gdb.zip. Do this infrequently.
[[ ! -f "cl2024_v1.gdb.zip" ]] \
   && wget -q -T 30 -t 3 https://www.fema.gov/about/reports-and-data/openfema/cl2024_v1.gdb.zip
# Create a list of DFIRMID's directly from NFHL data links, save as firm.gpkg. Do this once daily.
[[ ! -f "firm.gpkg" ]] \
   && echo -e "dfirmid,updated,locale\n""$(wget -q 'https://hazards.fema.gov/femaportal/NFHL/searchResult' -O- \
   | sed -n 's/.*ounty=\(.*\)&fileName=\(.*\)_\(.*\)\.zip.*/"\2","\3","\1"/p' |tr -s '"')" \
   | gdal vector convert -i /vsistdin/ --if CSV -o firm.gpkg --output-layer firm --overwrite
# Create nfhl.gpkg. Do this once daily.
gdal vector pipeline \
   concat firm.gpkg [ \
      ! read cl2024_v1.gdb.zip --input-layer NfipCommunityLayerNoOverlapsWhole \
      ! clip --bbox -126,24,-66,50 --bbox-crs EPSG:4326 \
      ! reproject --dst-crs EPSG:4326 \
      ! rename-layer --output-layer comm \
      ! sql --dialect sqlite --output-layer comm --sql "select cis_cid, county_fips as stcofips, county_fips||'C' as firmid, shape as geom from comm" \
   ] \
   ! write -o community.gpkg --overwrite \
   && gdal vector pipeline \
   ! read -i community.gpkg \
   ! sql --dialect sqlite --sql "$(cat<<EOF
      select
         f.dfirmid, c.stcofips, cast(f.updated as integer) as updated, f.locale, group_concat(c.cis_cid,',') as cids
         ,'https://hazards.fema.gov/femaportal/NFHL/Download/ProductsDownLoadServlet?DFIRMID=0&state=s&county=c&fileName='||f.dfirmid||'_'||f.updated||'.zip' as url
         ,st_union(c.geom) as geom
      from firm f
      join comm c on f.dfirmid = c.firmid or f.dfirmid = c.cis_cid
      group by f.dfirmid, c.stcofips, f.updated, f.locale
EOF
)" \
   ! simplify-coverage --tolerance .0001 \
   ! set-geom-type --multi \
   ! write -o nfhl.gpkg --output-layer nfhl --overwrite

# rm firm.gpkg

That's it!