How to tile heightmap chunk?

Hi,

I’m trying to create a terrain based on a heightmap and chunks.
I can currently add and remove chunks around my character and generate a heightmap based on the chunk’s position in the world (to keep it simple).
My problem is that I have no idea how to “join” the borders of the chunks together. Can someone enlighten me on the way forward?

here some code :
// Fill out your copyright notice in the Description page of Project Settings.

#include "WorldChunk.h"


// Sets default values
AWorldChunk::AWorldChunk()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	RootNode = CreateDefaultSubobject<USceneComponent>("Rootnode");
	RootComponent = RootNode;

	MeshComponent = CreateDefaultSubobject<URuntimeMeshComponent>(TEXT("ProceduralChunkMesh"));
	MeshComponent->SetupAttachment(RootComponent);

}

void AWorldChunk::SetupChunk(int32 inSeed, int32 inindexX, int32 inindexY, int32 inSectionSize, int32 inChunkSize, int32 inMaxHeight)
{
	Seed = inSeed;
	ChunkX = inindexX;
	ChunkY = inindexY;
	SectionSize = inSectionSize;
	ChunkSize = inChunkSize;
	MaxHeight = inMaxHeight; 
}

void AWorldChunk::OnConstruction(const FTransform & Transform)
{
	int32 NumberOfPoints = (SectionSize + 1) * (SectionSize + 1);
	int32 NumberOfVertices = SectionSize * SectionSize * 4; // 4x vertices per quad/section
	int32 NumberOfTriangles = SectionSize * SectionSize * 2 * 3; // 2x3 vertex indexes per quad
	Vertices.AddUninitialized(NumberOfVertices);
	Triangles.AddUninitialized(NumberOfTriangles);
	HeightValues.Reserve(NumberOfPoints);

	for (int i = 0; i < NumberOfPoints; i++) {
		HeightValues.Emplace(0.0f);
	}

	GenerateMesh();
}

// Generate heightmap
void AWorldChunk::GenerateHeightmap()
{
	noiseGenerator->setNoiseSeed(Seed);

	for (int32 x = 0; x < SectionSize + 1; x++) {
		for (int32 y = 0; y < SectionSize + 1; y++) {
			int nX = ((ChunkX * ChunkSize) + (x * SectionSize));
			int nY = ((ChunkY * ChunkSize) + (y * SectionSize));
			HeightValues[x + (y*SectionSize)] = noiseGenerator->SimplexNoise2D(nX, nY);
		}	
	}
}

void AWorldChunk::GenerateMesh()
{
	GenerateHeightmap();
	GenerateGrid(Vertices, Triangles, HeightValues);
	MeshComponent->ClearAllMeshSections();
	MeshComponent->CreateMeshSection(0, Vertices, Triangles, false, EUpdateFrequency::Infrequent);
}

void AWorldChunk::GenerateGrid(TArray<FRuntimeMeshVertexSimple>& InVertices, TArray<int32>& InTriangles, const TArray<float>& InHeightValues)
{
	FVector2D NumberOfSection = FVector2D(ChunkSize / SectionSize, ChunkSize / SectionSize);

	int32 VertexIndex = 0;
	int32 TriangleIndex = 0;
	
	for (int32 X = 0; X < SectionSize; X++) {
		for (int32 Y = 0; Y < SectionSize; Y++) {
			
			int32 BottomLeftIndex = VertexIndex++;
			int32 BottomRightIndex = VertexIndex++;
			int32 TopRightIndex = VertexIndex++;
			int32 TopLeftIndex = VertexIndex++;

			int32 NoiseIndex_TopLeft = (X + 1) + (Y * (SectionSize + 1));
			int32 NoiseIndex_TopRight = (X + 1) + (Y + 1) * (SectionSize + 1);
			int32 NoiseIndex_BottomRight = X + (Y + 1) * (SectionSize + 1);
			int32 NoiseIndex_BottomLeft = X + (Y * (SectionSize + 1));
			
			FVector pTopLeft = FVector((X + 1) * NumberOfSection.X, Y * NumberOfSection.Y, InHeightValues[NoiseIndex_TopLeft]);
			FVector pTopRight = FVector((X + 1) * NumberOfSection.X, (Y + 1) * NumberOfSection.Y, InHeightValues[NoiseIndex_TopRight]);
			FVector pBottomRight = FVector(X * NumberOfSection.X, (Y + 1) * NumberOfSection.Y, InHeightValues[NoiseIndex_BottomRight]);
			FVector pBottomLeft = FVector(X * NumberOfSection.X, Y * NumberOfSection.Y, InHeightValues[NoiseIndex_BottomLeft]);

			InVertices[BottomLeftIndex].Position = pBottomLeft;
			InVertices[BottomRightIndex].Position = pBottomRight;
			InVertices[TopRightIndex].Position = pTopRight;
			InVertices[TopLeftIndex].Position = pTopLeft;

			// Note that Unreal UV origin (0,0) is top left
			InVertices[TopLeftIndex].UV0 = FVector2D((float)(X + 1) / (float)SectionSize, (float)Y / (float)SectionSize);
			InVertices[TopRightIndex].UV0 = FVector2D((float)(X + 1) / (float)SectionSize, (float)(Y + 1) / (float)SectionSize);
			InVertices[BottomRightIndex].UV0 = FVector2D((float)X / (float)SectionSize, (float)(Y + 1) / (float)SectionSize);
			InVertices[BottomLeftIndex].UV0 = FVector2D((float)X / (float)SectionSize, (float)Y / (float)SectionSize);
			
			

			// Now create triangles 
			InTriangles[TriangleIndex++] = BottomLeftIndex;
			InTriangles[TriangleIndex++] = TopRightIndex;
			InTriangles[TriangleIndex++] = TopLeftIndex;

			InTriangles[TriangleIndex++] = BottomLeftIndex;
			InTriangles[TriangleIndex++] = BottomRightIndex;
			InTriangles[TriangleIndex++] = TopRightIndex;

			// Normals
			FVector NormalCurrent = FVector::CrossProduct(InVertices[BottomLeftIndex].Position - InVertices[TopLeftIndex].Position, InVertices[TopLeftIndex].Position - InVertices[TopRightIndex].Position).GetSafeNormal();

			// Normals
			InVertices[BottomLeftIndex].Normal = InVertices[BottomRightIndex].Normal = InVertices[TopRightIndex].Normal = InVertices[TopLeftIndex].Normal = FPackedNormal(NormalCurrent);

			// Tangents
			FVector SurfaceTangent = pBottomLeft - pBottomRight;
			SurfaceTangent = SurfaceTangent.GetSafeNormal();
			InVertices[BottomLeftIndex].Tangent = InVertices[BottomRightIndex].Tangent = InVertices[TopRightIndex].Tangent = InVertices[TopLeftIndex].Tangent = FPackedNormal(SurfaceTangent);
			
		}
	}
}

// Called when the game starts or when spawned
void AWorldChunk::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void AWorldChunk::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

Current result !